Skip to content

Operator runbook

Short reference for keeping a ProxiPort server healthy. Not a full admin guide — see install and migration for the first-run setup.

Service control

# Server
sudo systemctl status proxiportd
sudo systemctl restart proxiportd
sudo journalctl -u proxiportd -f

# Agent
sudo systemctl status proxiport
sudo systemctl restart proxiport
sudo journalctl -u proxiport -f

Paths

Purpose Path
Server config /etc/proxiport/proxiportd.conf
Server data /var/lib/proxiport/
Server SPA files /var/lib/proxiport/docroot/
Server logs (default) /var/log/proxiport/proxiportd.log (and journald)
Server PID file /run/proxiportd.pid
Agent config /etc/proxiport/proxiport.conf
Agent log journald (-u proxiport)

Backups

The server keeps everything that matters in two places:

  • SQLite databases under /var/lib/proxiport/. The set as of this writing: clients.db, monitoring.db, library.db, auditlog.db, api_sessions.db, api_token.db, client_groups.db, jobs.db, notifications.db, and the vault store at vault.sqlite.db. Each is created on demand the first time the corresponding feature is touched, so an early-life install may not have all of them on disk. Back the directory up with a stop-the-world snapshot or with the SQLite .backup command for hot backups.
  • /etc/proxiport/proxiportd.conf — the config with the pinned key_seed, jwt_secret, and admin credentials. Treat it as a secret in its own right, or encrypt those settings — see encrypting the config secrets.
  • The at-rest DEK key file (if [key_provider] type = "file" is configured) — losing it makes the encrypted totp_secret column, the entire vault, and any encrypted config setting unreadable. Back it up out-of-band, not only inside the data_dir tarball, and ideally keep it off the disk that holds the databases.

User auth lives either in a JSON file ([api] auth_file = "...") or in a table inside the main database ([api] auth_user_table = "users" plus a configured [database] connection), so back those up alongside the SQLite files as appropriate.

Tarball both directories for the simplest backup. Restore by stopping the service, replacing the directories, and starting again.

If you use MySQL instead of SQLite ([database] db_type = "mysql" in proxiportd.conf), back up the database with your usual MySQL tooling.

The vault is a special case. It is encrypted at rest with a passphrase that is never written to disk (and, when a [key_provider] is configured, additionally wrapped under the server DEK), so a server restart always re-locks it — back up vault.sqlite.db alongside the other databases and remember that restoring it requires re-entering the passphrase through the SPA before the documents and per-client secrets are readable again. If a key provider was enabled, the restored server must also present the same DEK, or the vault fails closed.

Vault locked — the empty state every operator sees after a server
restart, regardless of what is stored inside.

Per-client documentation page rendering the same locked-vault
banner — documents that live inside the vault stay opaque until the
passphrase is supplied.

Encrypting the config secrets

proxiportd.conf holds the two most valuable secrets on the box: key_seed, from which the SSH host key every agent pins is derived, and jwt_secret, which signs every API session. Anyone who reads the file in the clear can stand up a server your agents will trust and mint admin tokens for your API — from a copy, off your machine, without touching the running service.

Configure a key provider (the [key_provider] section of proxiportd.example.conf) and those settings can live in the config as ciphertext instead. Encrypt a value with:

proxiportd -c /etc/proxiport/proxiportd.conf secret encrypt

It prompts for the value without echoing it (or reads it from a pipe), and prints an enc:v1:… string. Paste that in place of the plaintext:

[server]
  key_seed = "enc:v1:0nQxk2…"

[api]
  jwt_secret = "enc:v1:kZ0m9y…"

The same works for [server] auth, [api] auth, [database] db_password, [smtp] auth_password, and the [pushover] credentials. Settings without the enc:v1: prefix are still read as plaintext, so a config can be migrated one value at a time, and nothing breaks if you encrypt none of them.

Two things follow from the DEK being the only way back:

  • Keep the DEK off the machine. A type = "file" key sitting next to the config it protects stops nobody who steals the disk. Prefer type = "env" with the key injected at start (systemd EnvironmentFile on a tmpfs, a secrets manager, your orchestrator).
  • A value the server cannot decrypt stops the server. Wrong DEK, missing DEK, corrupted ciphertext — proxiportd refuses to start rather than run with a regenerated jwt_secret (which would silently invalidate every session) or a different host key (which would break every agent's fingerprint check). Fix the key, don't work around it.

What else the key provider encrypts

Enabling a [key_provider] also encrypts the recoverable operational data the server stores, transparently — you read and write it through the API exactly as before, and only the on-disk databases hold ciphertext:

  • Command and script output (jobs.db): stdout, stderr and error text are encrypted. The command itself and the one-line summary stay in the clear so the schedules "last run" view works without the key.
  • Monitoring data (monitoring.db): the process list and mountpoint blobs (which can carry command-line arguments and filesystem layout) are encrypted.
  • Vault secrets and TOTP enrolment seeds, as covered in vault.md and the two-factor docs.

On the first boot after you enable a key provider, existing plaintext rows are re-encrypted in place. A stolen database read under the wrong key (or with none) fails closed — it never returns the ciphertext as if it were the value. As with the config secrets, this protects a stolen disk, not a live host that also holds the DEK.

File pushes are staged briefly on disk before agents pull them; that staging directory ({data_dir}/filepush) is created owner-only. For the strongest guarantee, mount it on a tmpfs so payloads never touch persistent storage.

Rotating credentials

  • Admin password. Edit /etc/proxiport/proxiportd.conf, change the [api] auth = "admin:<new-password>" line, restart the service. Or, if running a multi-user setup with auth_file, edit that JSON file or the corresponding DB row.

The Users page surfaces a banner whenever auth is pinned in the config ([api] auth = "admin:...") rather than user-managed — rotating in that mode is a config edit + restart, not a SPA action.

Static-auth warning on the Users page. - JWT secret. [api] jwt_secret = "<long-random-string>" — rotating this invalidates every issued session immediately. All users will be redirected to /auth. - key_seed. Rotating the seed changes the server's SSH host key. Every connected agent will fail the fingerprint check until its proxiport.conf is updated. Avoid rotating unless the seed is compromised; coordinate with all agent operators.

Both of these can be stored encrypted — rotate by writing the new value's enc:v1:… form into the config, exactly as above. - client-auth credentials. Used by agents to register. Change via [server] auth = "<id>:<password>" (single credential), or via the JSON file pointed at by [server] auth_file, or via the table named in [server] auth_table. Push the new credentials to each agent and restart it.

When auth_file mode is in use, the credentials are also manageable from the SPA at Client Access:

Client Access page — create, edit, and delete client-auth IDs
  used by agents to register.

  • API tokens. For programmatic API consumers, mint scoped API tokens at API Tokens rather than reusing a human's username/password. Tokens carry a lifetime and an explicit scope (read, read+write, …) and are revocable independently.

Create-token form — name, scope, and expiry. The token is
  shown once on creation; copy it
  immediately.

  • TOTP second factor. Set [api] totp_enabled = true in proxiportd.conf, restart, and each user is prompted to enroll on their next login. The user scans the SPA's QR with any RFC 6238 TOTP app:

2FA enrollment — the SPA renders the QR client-side; the secret
  never leaves the browser.

Admins can require an existing user to re-enroll from the Users page:

Picking a user to (re-)enroll for TOTP.

Once enabled at the server, the Info page reflects the new posture:

Server info with 2FA enabled and TOTP enabled flipped
  on.

Each user's profile shows their TOTP enrollment state and lets them reset it:

Profile page once 2FA is in
  effect.

Capacity and limits

  • Connected agents per server. SQLite handles a few hundred agents comfortably on modest hardware. For more, switch to MySQL.
  • Tunnel ports. [server] used_ports controls the pool of ports the server may allocate for tunnels. Default 20000-30000. Expand if you run out.
  • Per-user session lifetime. ?token-lifetime=<seconds> on /login; defaults to 10 minutes; the SPA asks for 24 hours.

Updating

  1. Stop the service.
  2. Replace the binary in /usr/local/bin/proxiportd (or wherever your package manager put it).
  3. Start the service. Schema migrations, if any, run on first start.
  4. Tail the logs to confirm.

For the agent, the same shape applies on each managed host.

Tunnels

A tunnel is a per-session listener on the server that forwards traffic back to a TCP port on the agent. Open one from a client's Tunnels tab.

Tunnel create form — pick the agent-side address, the public
scheme, and an ACL. The server allocates the public port from
[server] used_ports unless you pin it
manually.

The ACL is enforced at the server's listener, not at the agent — denied traffic never crosses the chisel session. Only my current IP address is the default preset and reads the request's X-Forwarded-For, falling back to the connection's remote address when that header is absent. If you front the server with a reverse proxy, configure the proxy to set X-Forwarded-For; with built-in TLS the connection IP is used directly.

Active tunnel with a single-IP ACL applied.

Tunnels also accept inactivity / lifetime caps, after which the server closes the listener and notifies the agent.

Tunnel timeouts — inactivity-close and hard-destroy
limits.

You can save a tunnel definition into the Stored tunnels library to re-open it later in one click.

Stored tunnels — pre-canned definitions for repeat
use.

The global Tunnels page is the operator's at-a-glance view across every connected agent.

Global active tunnels.

Finding and reusing tunnels from the API

The per-client endpoint GET /api/v1/clients/{id} includes that client's active tunnels by default. The list endpoint GET /api/v1/clients returns a lean projection — id, name, hostname — so request the tunnels field explicitly when you want it across the fleet:

GET /api/v1/clients?fields[clients]=id,name,tunnels

This is the same field-projection the list endpoint uses for every optional attribute: ask for a field and you get it; a client with no open tunnels reports an empty list. Each tunnel object carries the id you delete it by (DELETE /api/v1/clients/{id}/tunnels/{tunnelID}), so you never have to remember the id from when you created it.

Tunnels survive an agent restart

When an agent drops and reconnects under the same client id, the server re-establishes that client's tunnels automatically — including a manually pinned local port — and the restored listener starts carrying traffic again with no operator action. Because the tunnel is still there, opening a new tunnel to the same agent-side port returns ERR_CODE_TUNNEL_TO_PORT_EXIST. Reuse the existing tunnel (look it up as above) rather than recreating it; delete it by id first only if you genuinely want a fresh listener.

Commands and scripts

ProxiPort can run ad-hoc commands or multi-line scripts against any connected agent and stream the output back. The agent has to opt in via commands_allow / commands_deny in its proxiport.conf — running everything by default is intentionally not the shape.

Allowlist block — the agent rejected the command because no
matching commands_allow entry was in
proxiport.conf.

When a command is allowed, the SPA streams stdout / stderr live and prints the exit code at the end.

Single-host uname -a run.

Scripts are the same control surface with a multi-line editor and an interpreter selector (/bin/bash, /usr/bin/python3, …). The agent writes the body to a tempfile, executes, and reaps the process.

Script run — bash heredoc style, with timeout and
Run as sudo toggles, streamed output below.

For repeated patterns, save commands and scripts into the Library:

Saved-commands library.

Saved-scripts library.

A library entry can be run against multiple agents at once from the Commands page — useful for one-shot fleet work.

Multi-target command fan-out — same body, dispatched to every
selected agent in parallel.

The global Scripts page is the same concept for whole-fleet script runs. It is still being reimplemented from the upstream shape — at the moment it is a stub that links into the per-client Scripts tab.

Global Scripts stub.

Schedules

For recurring runs, attach a cron expression to a saved command or script. The scheduler is in-process; no external cron is needed.

Schedules — cron expression, target agent, and the body of the
command or script to dispatch.

File transfer

Push a file from the server's filesystem (or paste it inline) to a chosen path on the agent. The transfer goes over the same chisel session as everything else.

File push — pick the agent, the destination path, the mode, and
the body of the file.

Audit log

Every state-changing API call writes an auditlog.db row with the user, client (if any), action, timestamp, and the request payload. The per-client view filters the same table down to events touching one agent:

Per-client audit
log.

The global Audit page is for cross-cutting queries — who created which tunnel, who rotated which credential. Reading a vault secret — the one high-value read worth tracking — is recorded too, as a read action (never the secret itself).

Tamper-evidence

When a key provider is configured, each audit entry carries a keyed HMAC chained to the previous entry (the key is derived from the server DEK, so it is not on the audited box in usable form). Editing or deleting a row after the fact breaks the chain. Check it with:

curl -s -H "Authorization: Bearer $TOKEN" \
  https://<server>/api/v1/auditlog/verify | jq

A "Valid": true response means every entry verifies; a break reports the sequence number and kind (mac — a row was edited, link/gap — a row was removed or reordered). This detects tampering on the box; it does not stop a host with the DEK from forging new entries — ship entries to an off-host, append-only sink for that. Rotation starts a fresh chain per auditlog.<date>.db file.

Global audit log.

User and group admin

User groups gate which API endpoints (and therefore which SPA sections) each user can reach. The built-in Administrators group has every permission set and cannot be edited; build new groups by ticking only the columns the role needs. A user's effective permissions are the union across its groups.

User Groups — RBAC columns for tunnels, commands, scripts, scheduler,
monitoring, auditlog, uploads, and vault.

Client groups are operator-side labels you can attach to any agent. They are useful as targeting filters for the multi-target command page and for the audit-log query builder.

Client Groups list.

Each user has a profile page that shows their group membership and lets them rotate their own password.

Profile page in static-auth mode — password rotation is gated
behind the same [api] auth config edit when running pinned auth;
in auth_file mode it is a self-service action.

Common pitfalls

  • 429 too many requests immediately after a 401. ProxiPort inherits openrport's BanList: every failed auth attempt buys a 2-second deny on the keyed username. If the SPA fires three parallel API calls after a session expires, the first 401s and the rest 429. The SPA fixes this client-side by short-circuiting on a missing token; if you are writing your own client, do the same.
  • Agent refuses to connect with "fingerprint mismatch". Either the server's key_seed changed or the agent's fingerprint = "..." value is stale. Compare the fingerprint printed in the server log at startup against the agent config.
  • SPA shows the vault as locked even after entering the passphrase. Vault passphrases are not persisted server-side — they live in memory for the life of the process. A server restart re-locks the vault. Enter the passphrase again via Settings → Vault.

Where to get help

  • File a bug or feature request on the GitHub repository.
  • Private vulnerability reports go through GitHub Security Advisories — see SECURITY.md.