Scripts¶
Scripts are the multi-line cousin of commands. Where a command sends a single line for the agent's shell to evaluate, a script sends a body of code plus an interpreter — the agent writes the body to a tempfile, runs the interpreter against it, and reaps the process.
This lets you ship a bash heredoc, a Python script, or a PowerShell function to one or many agents and stream the output back, without keeping the script on disk on every host.
How it works¶
- The operator
POSTs the base64-encoded script body to/api/v1/clients/<id>/scriptsalong with an interpreter and the usual command options. - The server forwards the body to the agent via the chisel channel.
- The agent decodes the body, writes it to a random tempfile under
{data_dir}/scripts/, and invokes the interpreter against the file. - stdout/stderr stream back over the same channel.
- The agent deletes the tempfile when the process exits, regardless of the exit code.
The end-to-end model is the same as command execution; the only
differences are that scripts can hold multiple lines and that the
agent's [remote-scripts] switch must also be enabled (in addition
to [remote-commands]).
Enabling script execution on the agent¶
[remote-commands]
enabled = true
[remote-scripts]
enabled = true
If [remote-commands] enabled is false, scripts are also disabled —
script execution reuses the command-execution code path.
The allow/deny filter does not apply to scripts
A script body and its interpreter are not matched against
[remote-commands] allow or deny. The filter applies to the
command path only. A script job runs the interpreter the operator
chose, with the body the operator sent, and the only agent-side
control over that is [remote-scripts] enabled.
So narrowing allow does not narrow what scripts can do. If an
operator should not be able to run arbitrary code on a host, do not
give them the scripts permission, and leave
[remote-scripts] enabled = false on that agent.
Earlier releases of this page said the opposite. It was wrong: the agent has never applied the filter to script jobs.
The scripts directory¶
The agent writes each script body to {data_dir}/scripts/ and then runs
it. The path is not configurable — it is data_dir with scripts
appended — and two properties of that directory decide whether script
execution works at all.
It must not be readable or writable by anyone but the agent's own
account. Anything written there is executed as the agent, so a
group- or world-accessible scripts directory is a way to run code as
that account. The agent checks this at startup and refuses to run
anything while it is wider than 0700:
scripts directory /var/lib/proxiport-agent/scripts must be read-writable
only by proxiport-agent[997]. Change directory mode from 0755 to 0700.
Your setup is insecure
The packaged install and the pairing installer both create it correctly;
you only need this if you set data_dir yourself.
Its filesystem must allow execution. The agent execs the file it
writes, so a data_dir on a mount carrying noexec — common for
/var or /tmp on a hardened host, and the default for a Docker
tmpfs — fails every script job with:
failed to start a command: fork/exec
/var/lib/proxiport-agent/scripts/<uuid>: permission denied
Nothing else reports it, and the same host runs ordinary commands
fine: a command is handed to a shell to read, which needs no execute
bit. If scripts fail this way and commands do not, check the mount
options on data_dir.
Running a script¶
Bodies are base64-encoded because the JSON payload would otherwise break on embedded newlines, quotes, and special characters:
SCRIPT=$(printf '#!/bin/bash\nset -euo pipefail\nuptime\nfree -h\n' | base64 -w0)
TOKEN=$(curl -s -u admin:password \
https://proxiport.example.com/api/v1/login | jq -r .data.token)
CLIENT=alpha-prod
JOB=$(curl -s -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
--data-raw "{
\"script\": \"$SCRIPT\",
\"interpreter\": \"/bin/bash\",
\"timeout_sec\": 30
}" \
"https://proxiport.example.com/api/v1/clients/$CLIENT/scripts" \
| jq -r .data.jid)
curl -s -H "Authorization: Bearer $TOKEN" \
"https://proxiport.example.com/api/v1/clients/$CLIENT/commands/$JOB" | jq
Note that the result is fetched from the commands endpoint, not a separate scripts endpoint. Scripts are commands under the hood.
Request fields¶
| Field | Type | Notes |
|---|---|---|
script |
string (required) | Base64-encoded body. |
interpreter |
string | Default /bin/sh on Unix, cmd on Windows. See aliases below. |
cwd |
string | Working directory on the agent. |
is_sudo |
bool | Prefix with sudo -n on Unix. |
timeout_sec |
int | Agent-side supervision timeout. |
Multi-host execution¶
POST /api/v1/scripts fans a single script out to many agents:
curl -s -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
--data-raw "{
\"script\": \"$SCRIPT\",
\"interpreter\": \"/bin/bash\",
\"client_ids\": [\"alpha-prod\",\"bravo-staging\"],
\"group_ids\": [\"edge-fleet\"],
\"execute_concurrently\": true,
\"abort_on_error\": false,
\"timeout_sec\": 60
}" \
https://proxiport.example.com/api/v1/scripts
Concurrency and abort-on-error semantics are the same as command fan-out.
Interpreters and aliases¶
The server can ship any interpreter the agent can locate. Common shapes:
- Bash / sh / zsh —
/bin/bash,/bin/sh,/usr/local/bin/zsh. The agent invokes the interpreter against the tempfile path. - Python —
/usr/bin/python3. Use#!/usr/bin/env python3in the body if you want it to work across hosts with different paths. - PowerShell —
powershell(Windows PowerShell 5.x) or a full path likeC:\Program Files\PowerShell\7\pwsh.exe. ProxiPort appends-Noninteractive -executionpolicy bypass -Fileautomatically when the executable name containspowershell(case-insensitive). - CMD —
cmdon Windows. The agent writes a.battempfile.
For brevity, define aliases in the agent's
proxiport.conf
under [interpreter-aliases]:
[interpreter-aliases]
pwsh7 = 'C:\Program Files\PowerShell\7\pwsh.exe'
bash = 'C:\Program Files\Git\bin\bash.exe'
Now "interpreter": "pwsh7" in the API call resolves to the full path.
The library¶
The SPA's Library → Scripts page is a convenience wrapper around
/api/v1/library/scripts. A library entry stores the same fields as
an ad-hoc request — name, interpreter, sudo flag, cwd, and the body —
and can be run against one or many agents with a click. Library entries
are also the unit a schedule targets.
Manage library entries via the API:
# Create
curl -s -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
--data-raw '{
"name":"current_directory",
"interpreter":"/bin/sh",
"is_sudo":false,
"cwd":"/root",
"script":"pwd"
}' \
https://proxiport.example.com/api/v1/library/scripts
# List, sort by newest first
curl -s -H "Authorization: Bearer $TOKEN" \
'https://proxiport.example.com/api/v1/library/scripts?sort=-created_at' \
| jq
# Update (full replacement; partial updates are not supported)
curl -s -X PUT \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
--data-raw '{ "name":"...", "interpreter":"...", "is_sudo":false, "cwd":"...", "script":"..." }' \
https://proxiport.example.com/api/v1/library/scripts/<script-id>
# Delete
curl -s -X DELETE -H "Authorization: Bearer $TOKEN" \
https://proxiport.example.com/api/v1/library/scripts/<script-id>
The endpoint accepts sort= and filter[<field>]= on id, name,
created_by, and created_at.
WebSocket execution¶
For live stdout, the server exposes WS /ws/scripts. Send the same
JSON payload as the REST endpoint; receive each chunk as a frame. The
SPA uses this path so the operator sees output as it is produced
instead of waiting for the job to complete.
To enable the test harness UI at /api/v1/test/scripts/ui, set
enable_ws_test_endpoints = true in [api]. The test UI is the
easiest way to confirm a script body, interpreter, and target host
combination before wiring it into the SPA library or a scheduled job.
Security notes¶
- Scripts inherit the OS account that runs
proxiport. Use sudo rules to elevate only specific scripts, not all of them. - The body lands on disk briefly under
{data_dir}/scripts/. If the host has FIM (file-integrity monitoring), expect events from that directory during script execution. - The
send_back_limitcap from[remote-commands](default 4 MiB per stream) applies. Long-running, chatty scripts should redirect to a logfile on the host and tail it via another mechanism.
See also: command execution, client groups and permissions, operator runbook — schedules.