Why Sign Remote Commands in the Browser? Fleetd Authorization and Trust Boundaries
How Fleetd v0.3.0 signs remote commands with Ed25519, binds targets, prevents replay and enforces local permissions—and where browser and terminal trust ends.
Once a machine is connected to a dashboard, running commands from the browser is an obvious next step. Checking a disk, restarting a service or opening a terminal makes managing servers and routers easier. It also gives the dashboard the ability to change those machines. A monitoring outage and a compromised command interface have very different consequences.
The previous article covered outbound connections, device approval and identity. This article follows the next decision: if an agent already trusts its server, why verify a browser signature before running a command? The details below describe Fleetd v0.3.0. Signing adds an independent check, but its assumptions include the code served to the browser and what happens after an interactive terminal opens.
A dashboard session is not unlimited authority
In a conventional design, the administrator logs in, the server authorizes an operation, and the agent executes the server’s message. Execution authority is concentrated at the server.
Fleetd adds a local question: did a key trusted by this machine authorize this operation? The browser constructs and signs a request, the server forwards it, and the agent checks the signature against its own configuration. Login and connection authentication remain necessary; they answer different questions.
Confirm targets and command → Browser signs the request
→ Server forwards it → Agent checks authorization → Execution
A message arriving through an authenticated connection does not automatically qualify for execution.
Sign the whole operation
Signing only the word uptime leaves unanswered which machine may run it, when, and whether the same request can be submitted again. Fleetd signs an envelope containing these fields:
| Field | Purpose |
|---|---|
v |
Protocol version |
id |
Identify this authorization and detect repeated requests |
kind |
Command execution or terminal opening |
targets |
Authorized machine fingerprints |
argv or action |
Program and arguments, or a locally configured action |
issued_at, expires_at |
Acceptance window |
timeout |
Runtime timeout, subject to the agent’s limit |
The current command form wraps its input as ['sh', '-c', input]. Entering uptime signs a request containing those arguments. Pipes and && work because the interface explicitly invokes a shell.
The browser serializes the request into JSON bytes and signs them with Ed25519. The agent verifies those exact bytes before decoding the JSON. Changing uptime to reboot, or adding another target, invalidates the existing signature. If several machines were included originally, each may legitimately execute the request: target binding constrains scope rather than requiring a single target.
The agent evaluates the authorization
Each agent stores a trusted_signers list. A public key included with a request must already be trusted locally. Otherwise anyone could generate a key and authorize their own commands.
Beyond the signature, the agent checks the request type, its own fingerprint in the target list, timestamps and local permissions. Unknown JSON fields are rejected to avoid inconsistent interpretations of the request.
The browser creates requests valid for five minutes. The agent allows two minutes of clock skew and rejects declared validity intervals longer than ten minutes. Five minutes describes the acceptance window, not how long a command may run. Runtime timeout is separate. The skew allowance also means acceptance does not stop at an exact five-minute boundary. Both ends still need reasonably correct clocks.
Together these checks ask whether this trusted authorization applies to this machine now.
Replay protection is not exactly-once execution
An unchanged, correctly signed request can still be sent repeatedly. Fleetd gives each request an ID generated from 16 random bytes. After authorization succeeds, the agent records the ID and rejects another use of it.
The record is persisted in seen.json in the state directory, which defaults to /var/lib/fleetd. With that state retained, an ordinary process restart does not make still-valid requests reusable. If persistence fails, execution does not proceed.
This is not an exactly-once guarantee for business effects. The ID is recorded before the concurrency check and process launch. A request can therefore consume its ID without starting because all execution slots are occupied. Conversely, a command may already have changed the machine before a disconnect prevents the dashboard from receiving its result.
Clicking Run again creates a new ID and a new authorization. The agent cannot infer whether you are replacing a failed attempt or repeating a completed operation. For upgrades, configuration changes and database migrations, inspect the real state and design for idempotency where possible. State loss or disk rollback also differs from an ordinary process restart; persistent replay records do not magically survive either.
Local permission still applies
| Permission | What the agent accepts |
|---|---|
monitor |
Monitoring; no commands or terminals |
allowlist |
Trusted, signed requests for locally configured actions |
full |
Trusted, signed arbitrary commands and terminal requests |
A newly enrolled monitoring-only machine can stay on monitor. For a small set of maintenance actions, an allowlist can narrow authority. For example, on a systemd machine with nginx installed:
permission: allowlist
allowlist:
nginx-status: [systemctl, is-active, nginx]
This is only a permission configuration fragment; connection settings and trusted public keys are still required. A signed request carries action: nginx-status, while the agent obtains the actual executable and arguments from its own configuration. The sender cannot append arbitrary arguments, and a request cannot contain both action and argv.
The v0.3.0 command form sends shell commands and has no preset-action selector. Typing nginx-status into it will not invoke this action on an allowlist-only machine. The protocol and UI do not yet offer the same level of support.
Under the current deployment model the agent runs as root, so full commands have corresponding authority. A signature cannot determine whether a command is mistaken or destructive. Nor is local configuration immutable: an authorized root command can modify it. Local policy limits unauthorized requests, not the power of an already authorized root process.
Browser keys have a lifecycle
WebCrypto generates the Ed25519 key pair with a non-extractable private key, and IndexedDB stores the pair. The normal registration flow sends the public key to the server, not the private signing key.
Switching computers or clearing browser site data can therefore remove the ability to sign requests that existing agents recognize, even when login still works. A new browser needs a new key, and the agents must trust its public key. Registering it on the server does not automatically update existing installations.
Revocation also has to reach the agents. Deleting a public key from the dashboard does not revoke it on already configured machines. Remove it from their trusted_signers lists and restart the agents using the appropriate service manager.
The release-signing key is separate. Command signing authorizes an operation; release signing authenticates an agent update package. A later article will cover that update lifecycle.
Two important trust boundaries
The server also distributes the frontend
Non-extractable means normal WebCrypto APIs cannot export the private key material. It does not prevent malicious same-origin code from invoking the key to sign.
Fleetd serves its management UI from the server. A compromised server may distribute modified JavaScript during a later visit. That page could display one command and sign another, or otherwise misuse an existing key. A confirmation dialog supplied by the same page cannot independently prove the page is trustworthy.
The agent can reject modified requests without valid signatures, but trusted frontend code remains an assumption. Distributing a signing tool separately could reduce this dependency. The design document mentions a CLI signer; v0.3.0 does not implement it.
Only opening a terminal is signed
Opening a terminal uses a signed kind: shell request and requires local full permission. Subsequent keystrokes use the existing session without individual browser signatures. A compromised server could inject input into an already-open terminal.
Open interactive sessions when needed and close them afterwards. A clearly scoped one-shot command is often easier to review than an ongoing interactive session. The initial signature does not protect every later input in the same way.
Signing also does not hide commands or output. Encryption between agent and server protects the network path, but the server is an endpoint. This is not browser-to-agent confidentiality that excludes the server.
Look at rejection paths
Successful output is the obvious demo. Authorization also needs clear failure cases. Existing Fleetd tests cover modified payloads, untrusted keys, wrong targets, expired or future requests, insufficient permissions and duplicate IDs after replay state is reloaded. These tests constrain individual rules; they are not a complete security audit and do not automatically test malicious frontend delivery, terminal input or every deployment environment.
For a self-hosted operator, the useful result is an explainable chain: which keys are trusted, which machines were authorized, when a request is acceptable, what local policy permits and how duplicates are handled. The preset-action UI, independent signing and terminal protection remain concrete areas to improve.
Next comes a deployment question: how does the same Go agent run on both ordinary Linux servers and OpenWrt routers?
Discussion
Share a question or your experience. Comments appear after moderation. Your email is never displayed publicly. Comment privacy
Loading comments…