A simple secure server for letting trusted devices run commands on other trusted devices.
  • Rust 81.5%
  • Shell 18.5%
Find a file
microchipster ddae9c4ad4 cmdd
2026-01-14 23:02:58 -08:00
src cmdd 2026-01-14 23:02:58 -08:00
.gitignore cmd server 2026-01-12 17:55:08 -08:00
Cargo.toml cmdd 2026-01-14 23:02:58 -08:00
install fix permissions 2026-01-14 12:21:20 -08:00
README.md cmdd 2026-01-14 23:02:58 -08:00
uninstall installation 2026-01-14 10:36:56 -08:00

Command server

An HTTP server that executes pre-approved binaries on demand. Requests post a command and arguments; the server checks that the binary is allowed in ~/.config/cmdd/config.toml and runs it. Commands run in parallel unless a queue is specified in the request or config, in which case commands sharing that queue execute one-at-a-time.

Configuration

Create ~/.config/cmdd/config.toml (or pass --config with another path):

api_keys = ["super-secret-key"]

[[command]]
binary = "/usr/bin/echo"
queue = "echo-queue"
fork = true

[[command]]
binary = "/usr/bin/date"
allow_args = false

Fields:

  • api_keys (required): list of secrets clients must send via the X-API-Key header. Rotate these frequently and keep the file readable only by a trusted user.
  • binary (required): must match exactly what clients send.
  • queue (optional): commands with the same queue run serially; omit to allow parallel execution.
  • fork (optional, default false): if true, the server starts the command and returns immediately with status: "started".
  • allow_args (optional, default false): set to true to allow arguments; omit or false to reject request arguments for this command.
  • security.ip_allowlist (optional): comma-separated IP globs (default 127.0.0.1, ::1) that gate all requests. Use * to whitelist every address.
  • security.rate_limit (optional): configure max_requests and window_secs to throttle traffic per API key/IP pair.
  • security.throttle_delay_ms (optional): simple delay applied to every request to slow abusive clients.
  • security.max_failed_attempts and security.failed_attempt_window_secs: block API keys when invalid auth is repeated.
  • security.default_timeout_ms and security.max_output_bytes: control how long commands can run and how much stdout/stderr is returned (per-command overrides exist).
  • security.allow_ssh_hostnames (optional): when true, any HostName <ip> lines in ~/.ssh/config are merged into the allowlist, so SSH-managed devices can connect.
  • security.ip_allowlist (per-command, optional): overrides the global allowlist for that command.
  • timeout_ms (per command): override the default timeout.
  • max_output_bytes (per command): override the global output cap.

Running

cargo run -- --addr 0.0.0.0:8080
# optionally: cargo run -- --config /path/to/config.toml

Request format

POST http://<host>:<port>/execute with JSON body:

{
  "command": "/usr/bin/echo",
  "args": ["hello", "world"],
  "queue": "echo-queue"
}

Headers:

  • X-API-Key (required): must match one of the api_keys listed in the config (case-sensitive).

  • command (string): required, must appear in the config.

  • args (string array): optional, defaults to empty; rejected when allow_args = false for that command.

  • queue (string): optional; overrides the config queue when provided.

Response

  • Standard execution returns status: "ok" or "failed" with exit_code, stdout, and stderr populated.
  • When fork = true for the command, the server returns immediately with status: "started", exit_code: 0, and empty stdout/stderr while the process continues in the background.
  • Errors return 4xx with a plain text message.

Examples

Curl (queued and forked)

curl -X POST http://localhost:8080/execute \
  -H "Content-Type: application/json" \
  -d '{"command":"/usr/bin/echo","args":["hello"],"queue":"echo-queue"}'

Curl (no-args command)

curl -X POST http://localhost:8080/execute \
  -H "Content-Type: application/json" \
  -d '{"command":"/usr/bin/date"}'
# this will fail with 400 if args are supplied because allow_args = false

Rust (reqwest)

use reqwest::blocking::Client;
use serde::Deserialize;
use serde_json::json;

#[derive(Deserialize)]
struct CommandResponse {
    status: String,
    exit_code: i32,
    stdout: String,
    stderr: String,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::new();
    let resp: CommandResponse = client
        .post("http://localhost:8080/execute")
        .json(&json!({
            "command": "/usr/bin/echo",
            "args": ["from", "rust"],
            "queue": "echo-queue"
        }))
        .send()?
        .json()?;

    println!("status: {}", resp.status);
    println!("stdout: {}", resp.stdout);
    println!("exit: {}", resp.exit_code);
    Ok(())
}

Notes

  • Commands are executed exactly as provided; ensure absolute paths in the config for safety.
  • When no queue is set in the request or config, commands are dispatched in parallel.
  • When a queue is set, commands sharing that queue run serially while other queues continue in parallel.
  • Use fork = true when you only need to start a command and return immediately.
  • Use allow_args = false to lock down commands to their exact binary invocation.