Harden and rewrite for release
This commit is contained in:
@@ -1,19 +1,143 @@
|
||||
## Rex
|
||||
# Rex
|
||||
|
||||
Run the public and management servers:
|
||||
Rex is a small remote-execution relay. An API caller requests a named action; a connected device runs the matching, locally configured `argv` command. Commands never cross the network.
|
||||
|
||||
## What you need
|
||||
|
||||
- Python 3.13 or later and [uv](https://docs.astral.sh/uv/).
|
||||
- A server reachable by the devices that need to connect.
|
||||
- WSS/TLS for traffic beyond a trusted private network. Use the supplied Nginx example or configure TLS directly in Rex.
|
||||
|
||||
Install the project from its checkout:
|
||||
|
||||
```console
|
||||
uv run rex server
|
||||
uv sync
|
||||
```
|
||||
|
||||
Open the terminal management interface in another shell:
|
||||
Run each command below as `uv run rex ...` from the checkout, or install Rex into the appropriate environment and use `rex ...` directly.
|
||||
|
||||
## Quick start
|
||||
|
||||
1. Create the server configuration, allow the device name, and create two separate keys. The secret printed by `keys create` is shown only then.
|
||||
|
||||
```console
|
||||
uv run rex server devices create desk
|
||||
uv run rex server keys create desk-client --permission connect
|
||||
uv run rex server keys create automation --permission execute
|
||||
```
|
||||
|
||||
2. Copy [examples/server.toml](examples/server.toml) to `~/.config/rex/server.toml`; replace the placeholder secrets, adjust the listener addresses, and remove the example blacklist entries. The CLI already created this file, so edit it rather than overwriting it if you used step 1.
|
||||
|
||||
3. On the device, copy [examples/client.toml](examples/client.toml) to `~/.config/rex/client.toml`, set its `device_name` and the `desk-client` secret, then configure its local actions.
|
||||
|
||||
4. Start the server and client in separate environments:
|
||||
|
||||
```console
|
||||
uv run rex server run
|
||||
uv run rex client
|
||||
```
|
||||
|
||||
5. Invoke a registered action using the execution key:
|
||||
|
||||
```console
|
||||
curl --fail-with-body -X POST \
|
||||
-H "X-API-Key: EXECUTION_SECRET" \
|
||||
https://rex.example.net/device/desk/action/lock
|
||||
```
|
||||
|
||||
The endpoint returns `{"queued":true}` only when the named device is connected and currently has that action registered. A disconnect immediately removes all of its actions.
|
||||
|
||||
## Configuration
|
||||
|
||||
Rex creates `~/.config/rex/server.toml` and `~/.config/rex/client.toml` on first use, with owner-only file permissions. Supply another path with `--config` immediately after `server` or `client`, for example `rex server --config /etc/rex/server.toml run`. If the selected configuration is absent, Rex prints a warning naming the missing role and suggesting the other one; this catches a common `rex server` versus `rex client` typo before it quietly creates the wrong template.
|
||||
|
||||
### Server (`server.toml`)
|
||||
|
||||
Copy [examples/server.toml](examples/server.toml). All fields are optional except fields within a `[[keys]]` record.
|
||||
|
||||
| Setting | Default | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `public_host` / `public_port` | `127.0.0.1` / `8010` | Public HTTP API and device WebSocket listener. |
|
||||
| `management_host` / `management_port` | `127.0.0.1` / `8011` | Internal status API listener. Keep it loopback-only. |
|
||||
| `tls_certfile`, `tls_keyfile` | unset | Set both when Rex terminates TLS itself. Leave both unset behind Nginx. |
|
||||
| `rate_limit_per_minute` | `120` | Requests allowed per direct peer in a rolling minute. |
|
||||
| `blacklist` | `[]` | IP addresses or CIDR networks denied before authentication. |
|
||||
| `devices` | `[]` | Device names permitted to open a WebSocket. |
|
||||
| `[[keys]]` | none | A key’s name, secret, and permission list. |
|
||||
|
||||
Keys must have at least one permission:
|
||||
|
||||
| Permission | Allows |
|
||||
| --- | --- |
|
||||
| `connect` | Opening a device WebSocket. |
|
||||
| `execute` | Calling `POST /device/{device}/action/{action}`. |
|
||||
| `admin` | Reading the internal management status API; it also implies `connect` and `execute`. |
|
||||
|
||||
Use separate connect and execute keys. Keys are capability scoped, not device scoped: a connect key can connect as any name listed in `devices`, so issue one per client and revoke it when that client is retired.
|
||||
|
||||
### Client (`client.toml`)
|
||||
|
||||
Copy [examples/client.toml]. The client sends `device_name` and its action **names** during connection setup. Each `[[actions]]` entry has a `name` (letters, digits, `.`, `_`, or `-`; maximum 64 characters) and non-empty `argv` list. `argv[0]` is the executable. Rex uses `exec`, not a shell: shell syntax such as pipes, redirects, and `$VAR` expansion is deliberately unavailable.
|
||||
|
||||
`scheme` must be `ws` or `wss`. `port` is optional: omit it for a reverse proxy on the standard WSS port; set it for a direct listener such as `ws://192.0.2.10:8010`.
|
||||
|
||||
## TLS and Nginx
|
||||
|
||||
For an Nginx-terminated deployment, use [examples/nginx-rex.conf](examples/nginx-rex.conf), point the client at `wss://rex.example.net` with no port, and leave Rex TLS paths unset. The example exposes only the public listener on `127.0.0.1:8010`; do not proxy the management listener.
|
||||
|
||||
Rate limiting identifies Nginx as the direct peer, so all traffic through one proxy shares a bucket. Set a limit high enough for the expected aggregate traffic and use Nginx’s own per-client `limit_req` controls if per-client reverse-proxy limiting is needed.
|
||||
|
||||
For direct TLS, expose `public_host` deliberately (for example `0.0.0.0`), set both TLS paths, protect the private key, and configure clients with `scheme = "wss"` and `port = 8010`.
|
||||
|
||||
## Management CLI
|
||||
|
||||
The CLI changes the local server TOML; it does not call the network management API.
|
||||
|
||||
```console
|
||||
uv run rex manage
|
||||
rex server run
|
||||
rex server keys list
|
||||
rex server keys create laptop-client --permission connect
|
||||
rex server keys create deploy-bot --permission execute
|
||||
rex server keys create break-glass --permission admin
|
||||
rex server keys delete laptop-client
|
||||
rex server devices list
|
||||
rex server devices create laptop
|
||||
rex server devices delete laptop
|
||||
rex client
|
||||
rex client actions list
|
||||
rex client actions create lock -- /usr/local/bin/lock-screen
|
||||
rex client actions create wake-display -- /usr/bin/dpms force on
|
||||
rex client actions delete wake-display
|
||||
```
|
||||
|
||||
The manager connects to `http://127.0.0.1:8001` by default. Use
|
||||
`uv run rex manage --url http://host:port` to connect elsewhere. The footer
|
||||
shows all controls: `H/L` switch tabs, `h/j/k/l` move, `y` copies the selected
|
||||
value, `n` creates a key or device, `a` adds an action, `d` deletes the
|
||||
selection, `r` refreshes, and `q` quits.
|
||||
`rex client actions` changes the local `client.toml` action allow-list; it never sends a command to the server. Use `--` before the executable so its arguments are unambiguously part of the action, especially if an argument begins with `-`. There is no TUI. To protect a secret, do not pass it as a command-line value; `keys create` generates it and prints it once.
|
||||
|
||||
## HTTP API
|
||||
|
||||
The public API has no browsable OpenAPI/docs endpoints in production.
|
||||
|
||||
| Request | Required key | Result |
|
||||
| --- | --- | --- |
|
||||
| `POST /device/{device}/action/{action}` | `execute` | Forwards the action name when it is currently registered. |
|
||||
| `GET /keys` on the management listener | `admin` | Returns key names and permissions, never secrets. |
|
||||
| `GET /devices` on the management listener | `admin` | Returns allowed devices and actions registered by connected clients. |
|
||||
|
||||
Supply `X-API-Key` for every authenticated request. A missing/incorrect key or insufficient permission returns `401`; a blacklisted or rate-limited peer returns `429`; an offline device or unavailable action returns `404`.
|
||||
|
||||
## Security and operations
|
||||
|
||||
- Use `wss` outside a fully trusted network. WebSocket/API keys are bearer credentials.
|
||||
- Run each client under a dedicated, minimally privileged OS account. Anyone who can edit its TOML can configure a command for that device.
|
||||
- Client commands are local allow-list entries. Remote callers cannot provide arguments or commands.
|
||||
- Keep the management API on loopback. Its admin-key requirement is defense in depth, not a reason to expose it.
|
||||
- Treat IP blacklisting as a coarse control, not authentication. Revoke compromised keys with `rex server keys delete NAME`.
|
||||
- Review device action definitions before deployment. Rex intentionally runs the locally configured commands with the permissions of its client service account.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Check |
|
||||
| --- | --- |
|
||||
| Client cannot connect | Device name is listed on the server, the key has `connect`, client URL/scheme/port match the listener, and the TLS certificate is valid for the endpoint. |
|
||||
| Invocation returns 404 | The client is offline or did not register that exact action name. Check the client TOML and service logs. |
|
||||
| Invocation returns 401 | Use an `execute` (or `admin`) key in `X-API-Key`; do not use the client’s `connect` key. |
|
||||
| Invocation returns 429 | Check `blacklist` and the rolling limit. Behind Nginx, its aggregate proxy bucket may be full. |
|
||||
|
||||
Reference in New Issue
Block a user