A small remote management agent for a droplet. It runs as a root systemd service and exposes an HTTPS control plane that lets an authorized caller update the agent, run commands on the box, and install an app from a git repo.
It uses the Python standard library only — Ubuntu ships python3, so there is
nothing to install. The idle process uses roughly 12–18 MB of RAM.
| Method | Path | Auth | Source IP | Purpose |
|---|---|---|---|---|
| GET | / |
no | any | Liveness probe. Returns {"status":"ok"}. |
| GET / POST | /update |
yes | any | git pull this checkout, then run its update.sh. |
| GET / POST | /command |
yes | allowed IP | Save the request body to /tmp/command_<dd_mm_yy_hh_mm_ss>.sh, make it executable, and run it as root. |
| POST | /install-app |
yes | any | Launch install_app.sh in the background to install an app + dependencies. Returns once the job is spawned. |
| GET | /progress |
yes | any | The live install log (setup.log). Empty when nothing is installing. Poll it (~every 5s) to follow an install. |
| GET | /supported |
yes | any | The supported_deps.json registry of installable dependencies. |
| GET | /apps |
yes | any | The installed_apps.json list of installed apps. |
curl -X POST "https://<ip>:5005/update" -H "Authorization: Bearer $TOKEN"Runs git -C /opt/p5agent pull --ff-only (if the pull is refused, the repo is
reset to HEAD), then bash /opt/p5agent/update.sh. The response status comes
from update.sh's exit code (200 on success, 500 on failure).
The command is read from the request body. If it does not start with a shebang,
#!/usr/bin/env bash is prepended. It is written to the tmp directory with a
timestamped name, given 0700 permissions, and executed as root. This endpoint
is restricted to P5AGENT_ALLOW_IP (returns 403 from any other source IP).
curl -X POST "https://<ip>:5005/command" \
-H "Authorization: Bearer $TOKEN" \
--data-binary $'systemctl restart myapp\nsystemctl is-active myapp'Installs an app from a git repo plus a list of dependencies. It does not
accept raw scripts — only a repo (with a key if private) and named dependencies
drawn from supported_deps.json. The agent itself does no installing: it writes
the request to a temp file and launches install_app.sh detached, returning
{"status":"started"} as soon as the job is spawned. Progress is followed via
/progress.
curl -X POST "https://<ip>:5005/install-app" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"repo": "https://github.com/user/app.git",
"key": "<token-for-private-repo>",
"branch": "main",
"name": "app",
"product-name": "My App",
"port": "8080",
"dependencies": ["postgresql", "swift 6"]
}'Only repo is required. install_app.sh then, logging every step to
setup.log with per-line timestamps:
- Installs dependencies. Each name is looked up in
supported_deps.jsonand installed via itspackage-manager(apt) orinstall-cmd. No dependencies → nothing installed. Each is logged as<name> installation began … <name> installation completed. - Clones the repo into
/opt/<name>(the key is embedded for a private clone and never echoed back). - Runs the setup script —
setup.shorinstall.shfrom the repo, if present. - Records the app in
installed_apps.jsonand writesSetup completed., then movessetup.logto<tmp>/x_setup_<timestamp>.log(so/progressgoes empty — the signal that nothing is running).
A version may be appended after a space (e.g. swift 6, ruby 3.4.5). Most
dependencies install via apt; swift is fetched from swift.org. The full,
current list comes from /supported.
Progress & concurrency. While an install runs, setup.log is updated and
served by /progress. A second /install-app while one is active is a no-op.
If setup.log is stale (untouched > 10 min), the next run waits 30s and, if
still unchanged, treats the previous install as failed — archiving it to
<tmp>/x_setup_<timestamp>-failed.log and clearing setup.log.
Every request except / must carry the shared secret token in the
Authorization header (never in the URL, so it stays out of logs and history):
Authorization: Bearer <TOKEN>
The token is compared in constant time. If no token is configured, the agent
rejects every privileged request with 401. /command additionally requires
the request to originate from P5AGENT_ALLOW_IP.
The agent reads its configuration from the environment (install.sh writes it
to /etc/p5agent.env, mode 600):
| Variable | Default | Meaning |
|---|---|---|
P5AGENT_TOKEN |
(empty) | Shared secret required on privileged endpoints. |
P5AGENT_ALLOW_IP |
127.0.0.1 |
Client IP allowed to call /command. |
P5AGENT_PORT |
5005 |
Listen port. |
P5AGENT_BIND |
0.0.0.0 |
Listen address. |
P5AGENT_DATA_DIR |
/var/lib/p5agent |
Runtime state: setup.log, installed_apps.json. |
P5AGENT_TMP_DIR |
/tmp |
Where /command scripts and archived install logs go. |
P5AGENT_TIMEOUT |
1800 |
Max seconds any command may run. |
P5AGENT_TLS_CERT |
(empty) | TLS certificate (PEM). If unset, install.sh generates a self-signed one. |
P5AGENT_TLS_KEY |
(empty) | TLS private key (PEM). If unset, install.sh generates a self-signed one. |
The agent always serves HTTPS (TLS 1.2+). Point P5AGENT_TLS_CERT/KEY at your
own PEM files, or leave them unset and install.sh generates a self-signed
certificate (for the droplet's IP) under /opt/p5agent/certs.
| File | Where | Purpose |
|---|---|---|
agent.py |
repo | The HTTP agent. |
update.sh |
repo | Restarts the service to apply a pulled update. |
install_app.sh |
repo | Backgrounded app installer (deps + clone + setup). |
install_swift.sh |
repo | Dedicated Swift installer; referenced by the swift entry's install-cmd. |
supported_deps.json |
repo | Registry of installable dependencies: name, display-name, icon-url, and a package-manager (+ optional package) or install-cmd. Served by /supported. |
setup.log |
data dir | Live install log; served by /progress. |
installed_apps.json |
data dir | Installed apps (name, product-name, path, port, dependencies); served by /apps. |
To add a new installable dependency, add an entry to supported_deps.json —
there is no per-package code. An entry either names a package-manager (apt,
with an optional package if it differs from name) or carries an
install-cmd. An install-cmd is one of:
- an inline shell one-liner, with
{version}substituted from the request; or - a local script — any value ending in
.shis resolved against the repo and run with the requested version as its argument (e.g. Swift usesinstall_swift.sh, which receives6or6.0.3).
The layout follows the Filesystem Hierarchy Standard (FHS), the standard Ubuntu convention for where a service keeps its files:
| Path | FHS role | Used for |
|---|---|---|
/opt/p5agent |
add-on application software | the agent's code (this checkout) |
/etc/p5agent.env |
host configuration | the agent's config, mode 600 |
/var/lib/p5agent |
persistent application state | setup.log and installed_apps.json |
/tmp |
temporary files | archived install logs (x_setup_<ts>.log) and /command scripts |
/opt/<name> |
add-on application software | installed apps |
Run as root from a checkout of this repo:
P5AGENT_TOKEN=<secret> P5AGENT_ALLOW_IP=<dashboard_ip> bash install.shOnly P5AGENT_TOKEN is required. The script ensures git and openssl are
installed, copies the agent to /opt/p5agent, generates a self-signed TLS cert
(unless one is provided), writes /etc/p5agent.env, installs and starts the
p5agent systemd service, and configures the firewall.
The firewall (UFW) is reset to deny all incoming by default, with these ways in:
- SSH (
22) — open to all, so admins and the DigitalOcean console can always reach the box; - the agent port (
5005) — open to all hosts (per-endpoint source-IP enforcement is done inside the agent, only/commandis IP-locked); - a port per installed app — re-opened on every run from the
portof each entry ininstalled_apps.json.
Every other port is closed. (P5AGENT_ALLOW_IP only governs who may call /command)
systemctl status p5agent # service state
journalctl -u p5agent -f # live logsThe agent runs arbitrary commands as root, so the token and the network boundary are what protect it:
- Serve over HTTPS (
P5AGENT_TLS_CERT/P5AGENT_TLS_KEY) so the token and commands are never sent in cleartext. - Keep
P5AGENT_TOKENlong and secret; it is the entire access control. - Set
P5AGENT_ALLOW_IPto the dashboard's IP so/command(raw root commands) is reachable only from there; it defaults to localhost.