Command execution¶
ProxiPort can dispatch a command to one or many connected agents, stream the output back, and record the result. The command travels through the existing chisel control channel — no tunnel is opened, and no extra port on the agent host is exposed.
For multi-line bodies and stored, reusable invocations, use scripts instead. The two share the same allow/deny filter on the agent.
How it works¶
- The operator (SPA, REST API, or a custom client)
POSTs a command body to/api/v1/clients/<id>/commands(single host) or/api/v1/commands(multi-host). - The server checks the user's
commandspermission and the per-client ACL, then forwards the body to the agent. - The agent matches the command against its
[remote-commands]allow/deny lists and, if accepted, spawns the process under the OS account that runsproxiport. - The agent streams stdout/stderr back over the chisel channel; the
server stores the result in
jobs.dband pushes it to any open WebSocket subscribers.
The exit code, started/finished timestamps, PID, and the executing user are all preserved on the job record.
Single-host execution¶
CLIENT=alpha-prod
TOKEN=$(curl -s -u admin:password \
https://proxiport.example.com/api/v1/login | jq -r .data.token)
JOB=$(curl -s -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
--data-raw '{"command":"uname -a","timeout_sec":10}' \
"https://proxiport.example.com/api/v1/clients/$CLIENT/commands" \
| jq -r .data.jid)
# Fetch the result once it completes
curl -s -H "Authorization: Bearer $TOKEN" \
"https://proxiport.example.com/api/v1/clients/$CLIENT/commands/$JOB" | jq
A representative response:
{
"data": {
"jid": "f72b69fd-f418-40c3-ab62-4ce2c2022c58",
"status": "successful",
"client_id": "alpha-prod",
"command": "uname -a",
"cwd": "/home/proxiport",
"is_sudo": false,
"interpreter": "/bin/sh",
"pid": 908526,
"started_at": "2026-05-17T15:30:12.934Z",
"finished_at": "2026-05-17T15:30:12.937Z",
"created_by": "admin",
"timeout_sec": 10,
"result": {
"stdout": "Linux alpha 6.8.0-111-generic ...\n",
"stderr": ""
}
}
}
timeout_sec is the agent's supervision window. If the process exceeds
it, the job is marked unknown but the process is not killed — that is
intentional, because killing a long-running migration mid-step is
usually worse than letting it finish unsupervised.
Job body fields¶
| Field | Type | Notes |
|---|---|---|
command |
string (required) | The command line to execute. Must satisfy the agent's allow/deny filter. |
timeout_sec |
int | Agent supervision timeout, default 60. |
cwd |
string | Working directory on the agent. |
is_sudo |
bool | Prefix with sudo -n on Unix. Requires an NOPASSWD sudoers entry — see below. |
interpreter |
string | Override the default shell. Useful for powershell on Windows or a [interpreter-aliases] entry. |
Multi-host execution¶
POST /api/v1/commands fan-outs a single command body to any combination
of client IDs and client-group IDs:
curl -s -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
--data-raw '{
"command": "/usr/bin/uptime",
"client_ids": ["alpha-prod", "bravo-staging"],
"group_ids": ["edge-fleet"],
"execute_concurrently": true,
"abort_on_error": false,
"timeout_sec": 30
}' \
https://proxiport.example.com/api/v1/commands
The response carries one parent jid and one child job per target
host. Poll /api/v1/commands/<jid> to get the rolled-up status, or
subscribe to the WebSocket endpoint for live stdout.
Execution-flow flags:
execute_concurrently
: false by default — agents run in series, useful for migrations
where one host must finish before the next starts. true fans the
command out in parallel.
abort_on_error
: true by default in sequential mode — the first failure stops the
rest. Ignored in concurrent mode. Set to false to keep going
regardless.
For group IDs, define the group first via the client-groups API.
Authorising commands on the agent¶
Allowing arbitrary command execution from the API is not the default.
Each agent's
proxiport.conf
has a [remote-commands] block with an allow/deny filter:
[remote-commands]
enabled = true
allow = [
'^/usr/bin/.*',
'^/usr/local/bin/.*',
'^C:\\Windows\\System32\\.*'
]
deny = ['(\||<|>|;|,|\n|&)']
order = ['allow', 'deny']
send_back_limit = 4194304
The filter is applied to the full command line as sent. Two practical consequences:
- The operator must invoke commands by absolute path.
uname -ais not in/usr/bin/.*unless sent as/usr/bin/uname -a. - Shell metacharacters are rejected by the default
denyregex — pipes, redirects,;,&&, etc. Wrap multi-step work in a script instead.
Order semantics¶
order = ['allow', 'deny'] (the default):
- The command must match at least one
allowregex. - If it also matches a
denyregex, it is rejected. - Otherwise it is allowed.
order = ['deny', 'allow']:
- If the command matches a
denyregex, it is rejected unless it also matches anallowregex. - Anything that matches no regex at all is allowed.
The first order is "default deny", the second is "default allow". Use the first one unless you have a specific reason.
Examples¶
Allow anything under the standard binary dirs and any sudo -n
invocation:
allow = [
'^/usr/bin/.*',
'^/usr/local/bin/.*',
'^sudo -n .*'
]
Allow a single specific script:
allow = ['^/opt/site/maintenance.sh( |$)']
Allow PowerShell scripts under a fixed directory on Windows:
allow = [
'^C:\\Windows\\System32\\.*',
'^C:\\Users\\Administrator\\scripts\\.*\\.bat'
]
Sudo / privileged commands¶
The agent runs as an unprivileged user on Linux and as the local
service account on Windows. To allow is_sudo: true invocations on
Linux, add an NOPASSWD sudoers rule scoped to exactly the commands
the agent should be able to elevate:
# /etc/sudoers.d/proxiport
proxiport ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart nginx
proxiport ALL=(ALL) NOPASSWD: /usr/local/bin/maintenance.sh
Avoid a blanket proxiport ALL=(ALL) NOPASSWD: ALL — at that point
the allow/deny filter is the only thing standing between the API and
full root.
Output size limits¶
The default send_back_limit is 4 MiB per stream. Anything beyond that
is truncated and the job result carries a truncated flag. Raise it
in [remote-commands] if you need full output, or write the output to
a file on the agent and pull it back via a separate path.
API surface¶
| Endpoint | Purpose |
|---|---|
POST /api/v1/clients/<id>/commands |
Single-host command |
GET /api/v1/clients/<id>/commands/<jid> |
Single-host result |
POST /api/v1/commands |
Multi-host fan-out |
GET /api/v1/commands/<jid> |
Multi-host result |
GET /api/v1/library/commands |
Stored command library |
WS /ws/commands |
Live stdout/stderr stream |
See also: scripts, client groups and permissions, operator runbook — commands and scripts.