Skip to main content
Every plugin the agent installs by default must be signed by an Ed25519 key the agent trusts. This page covers the publisher-side lifecycle: generation, signing, the on-device trust list, and revocation.

Why Ed25519

Small keys (32 bytes), small signatures (64 bytes), constant-time verification, no parameter choices to misconfigure. The same algorithm and verifier sign OTA payloads, so the agent’s verification code already exists and is well-tested.

Generate a key pair

The agent CLI mints a keypair in one step:
This writes two files under keys/:
  • acme-2026-A.pem is the public key (mode 0644). This is what operators install at /etc/ados/plugin-keys/<signer-id>.pem.
  • acme-2026-A.priv.pem is the private key (mode 0600). The signing rig keeps this. Never check it in, never copy it across hosts without an encrypted transport.
The first positional argument is the signer id. Pass --force to overwrite existing files at the target paths. keygen prints a SHA-256 fingerprint of the public key so an operator can cross-check the installed key against what you generated.
keygen is a developer aid. Production publisher keys deserve a hardware-token or air-gapped workflow, not a one-liner. See Key storage.
The reference repo template at altnautica/ADOSExtensions carries a .gitignore that covers keys/*.pem.

Alternative: openssl

openssl produces an equivalent PEM keypair:
The public PEM (acme-2026-A.pem) is what operators add to their trust list; its basename is the signer id. Keep the private key out of git.

Pick a signer key id

The signer key id is a short, stable label. It is the first line of the archive’s SIGNATURE file and the basename of the public key in the trust list. Operators see it on the install dialog. Use a namespace prefix and a generation suffix, for example acme-2026-A. The id is not a secret. It is a stable identifier for trust management.

Pack and sign

ados plugin sign packs a plugin directory and signs it in one command:
All four are required: the plugin directory (it must contain a manifest.yaml), --key (the Ed25519 private key in PEM), --signer-id (written to the SIGNATURE file, and it must match the public-key filename on the agent), and --output (the path for the signed archive). The command computes the canonical payload hash (SHA-256 over the sorted <path>\n<sha256-hex>\n lines of every entry except SIGNATURE), signs that 32-byte digest with the Ed25519 private key, and writes a SIGNATURE file (the signer id on the first line, then the base64 signature) into the archive. Because the hash covers every entry, the signature protects the manifest and every asset at once.

Alternative: pack.sh and sign.sh (monorepo CI)

The altnautica/ADOSExtensions repo ships two scripts for its tagged release pipeline. pack.sh builds the GCS bundle and zips the archive; sign.sh adds the signature.
sign.sh writes a *.signed.adosplug archive next to the input. ADOS_SIGNING_KEY is a path to the private key (PEM or raw 32-byte). The signer id defaults to altnautica-2026-A when ADOS_SIGNING_KEY_ID is unset. The payload hash and SIGNATURE format are identical to the CLI, so an archive from either path verifies the same way on the agent.

Trust list on the agent

The agent trusts a directory of PEM public keys:
Each file is a PEM public key whose basename is the signer id. The agent verifies an archive’s signature against the key whose id matches the SIGNATURE file’s first line. A signature from a signer id with no matching file is rejected. After adding or removing a key, restart the supervisor:
A short hardcoded allowlist of first-party signer ids (altnautica-2026-A, altnautica-2026-B) is maintained in agent code, not by filename, so a planted key file cannot impersonate first-party status. First-party status is what unlocks the in-process agent isolation and inline GCS isolation modes; third-party plugins always run subprocess and iframe.

Revocation

Revoked signer ids live in a JSON file on the agent:
The file is a flat JSON array of signer ids:
A plugin signed by a revoked id refuses to load. Revocation is the response when a key is compromised: add the id to the list, re-sign your current releases with a fresh key, and notify operators. For the full incident flow, see Revocation and incidents.

CI signing

A tagged release can sign in CI with the key held in a repository secret. Pass the base64-encoded private key inline:
ADOS_SIGNING_KEY_INLINE=1 tells sign.sh to treat ADOS_SIGNING_KEY as a base64 key body rather than a file path; it writes it to a tempfile, signs, and wipes it. Hold the secret in a protected environment with required reviewers if the key is high-value.

Key storage

For developer iteration, a file on disk is fine. Pin tight permissions and never commit it:
For production keys, keep the private key off general-purpose disks. A hardware security module, a smartcard with an Ed25519 applet, or a TPM-sealed key all reduce the blast radius of a host compromise. The signing input is just the 32-byte payload digest, so any tool that can produce an Ed25519 signature over that digest can replace sign.sh in a hardened pipeline.

Self-signed for closed deployments

A fleet that never pulls from a public registry signs and trusts end to end without Altnautica involvement:
  1. Generate a key pair locally.
  2. Place the public key at /etc/ados/plugin-keys/<your-id>.pem on every agent and restart the supervisor.
  3. Sign your plugins with the matching private key.
  4. Distribute via internal HTTPS, USB, or your existing fleet channel, then ados plugin install <path> against the local archive file.
No outbound calls, no external trust dependency. The trust chain is operator-end-to-end.

See also