Initial server source import
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
# Production edge deployment
|
||||
|
||||
This opt-in deployment keeps PostgreSQL, supervisor control, the standalone dashboard,
|
||||
the Worker API, and its typed admin backend on the runtime container's loopback. The
|
||||
root-owned deployment profile selects one of two exact Caddy topologies. Both keep the
|
||||
private Caddy admin API on an unpublished Unix socket and preserve the same Worker API,
|
||||
admin authentication, operator attribution, denylist, and header contract.
|
||||
|
||||
## Deployment profiles
|
||||
|
||||
If `/etc/truf/deployment-profile` is absent, `standalone-edge-v1` is selected. The
|
||||
standalone profile publishes runtime TCP 443 and gives the managed edge only
|
||||
`NET_BIND_SERVICE`.
|
||||
|
||||
For a host whose existing root-owned Caddy must remain the sole owner of ports 80/443,
|
||||
install the shared profile before running the host-agent installer:
|
||||
|
||||
```sh
|
||||
printf '%s\n' shared-host-edge-v1 | sudo install -m 0444 -o root -g root /dev/stdin /etc/truf/deployment-profile
|
||||
```
|
||||
|
||||
`shared-host-edge-v1` runs the runtime in the host network namespace with no Docker
|
||||
published ports. The managed Truf edge shares that namespace, has no capabilities, and
|
||||
binds plain HTTP only at `127.0.0.1:18766`. The existing host Caddy imports the fixed
|
||||
route-only `deploy/edge/host-caddy-shared.caddy` snippet inside the reviewed public site.
|
||||
Install that import before any catch-all handler. It handles only `/api/v1/worker/*` and
|
||||
the exact random admin prefix; it does not define a listener, TLS policy, global option,
|
||||
or route for another application. The host agent never restarts or reconfigures host
|
||||
Caddy or X-UI.
|
||||
|
||||
Profile changes are maintenance operations: stop the host agent first, require no active
|
||||
apply or failed hold, install the exact root-owned mode-0444 value, validate the selected
|
||||
Compose projection and host-Caddy configuration, then restart the agent. Never expose
|
||||
profile selection through the admin or host-agent request.
|
||||
|
||||
### Choosing a topology
|
||||
|
||||
Use `standalone-edge-v1` on a dedicated host where the managed edge can own public TCP
|
||||
443. The request path is:
|
||||
|
||||
```text
|
||||
Internet -> managed Caddy :443 -> private runtime :8766
|
||||
```
|
||||
|
||||
Use `shared-host-edge-v1` only when an existing root-owned Caddy must remain the sole
|
||||
owner of public ports and TLS. The request path is:
|
||||
|
||||
```text
|
||||
Internet -> host Caddy :443 -> 127.0.0.1:18766 -> managed Caddy -> private runtime :8766
|
||||
```
|
||||
|
||||
Shared-host mode adds a one-time integration boundary, not a second public edge. The
|
||||
operator installs the fixed route snippet, places its import before every catch-all,
|
||||
supplies the independent ingress marker, and validates the complete host Caddy
|
||||
configuration. After that bootstrap, runtime restart and document apply use the same
|
||||
host-agent lifecycle as standalone mode. The host agent never owns the host Caddy
|
||||
configuration or service lifecycle.
|
||||
|
||||
These are the only supported production topologies. Nginx, Traefik, an arbitrary Caddy
|
||||
layout, or an ad-hoc Compose override is not equivalent to either profile. Add and test a
|
||||
new exact deployment profile instead of translating private headers approximately. The
|
||||
host agent validates the selected fixed Compose projection and rejects metadata drift.
|
||||
|
||||
The shared-host projection currently carries the constrained-host runtime limits declared
|
||||
in `compose.shared-host.yaml`. A materially different CPU or memory envelope also requires
|
||||
a reviewed profile change; do not hide it in an unvalidated local override.
|
||||
|
||||
### End-to-end host bootstrap
|
||||
|
||||
The repository provides fixed deployment components, not a universal VPS installer,
|
||||
Ansible role, public image registry, or infrastructure module. Bootstrap a new host from
|
||||
one reviewed release checkout as follows:
|
||||
|
||||
1. Install the reviewed Linux, Docker Engine and Compose plugin, systemd, Python 3, and
|
||||
fail2ban prerequisites; provision DNS and the selected TLS ownership boundary.
|
||||
2. Install the release checkout root-owned at `/opt/truf` and choose exactly one deployment
|
||||
profile before installing the host agent.
|
||||
3. In shared-host mode, install the fixed host-Caddy import, validate the complete host
|
||||
configuration, and prove an unavailable loopback edge cannot fall through to another
|
||||
application.
|
||||
4. Create the protected edge directories, denylist state, and mode-0600 edge environment
|
||||
described below. Generate independent admin, edge, and shared-ingress values rather
|
||||
than copying values from another host.
|
||||
5. Install and validate the fixed host agent. Its first install seeds an absent active
|
||||
config from `app/config.linux.yaml` and an absent secrets document as an empty mapping;
|
||||
repeat installation never replaces active documents.
|
||||
6. Install every trusted worker-package manifest referenced by the runtime config beneath
|
||||
`/etc/truf/worker-packages` with the exact ownership and mode described below.
|
||||
7. Review the private active config and secrets, then build `runtime` and `edge` from the
|
||||
same checkout with the exact base and selected profile Compose files.
|
||||
8. Start the stack, explicitly enable Worker API and admin only after their private
|
||||
configuration is complete, and verify PostgreSQL, runtime, edge, HTTPS, admin, Worker
|
||||
API, host-agent, and unrelated host applications.
|
||||
|
||||
Initial host bootstrap is therefore intentionally more manual than later operation.
|
||||
Normal config apply, restart, rollback, status, and audit are performed through the typed
|
||||
control plane and fixed host agent after this trust boundary is established.
|
||||
|
||||
## Host agent and fixed runtime paths
|
||||
|
||||
Install the root-owned checkout at `/opt/truf`. Before invoking the host-agent installer,
|
||||
prepare the edge state below and create the complete protected environment file that its
|
||||
fixed combined-Compose validation consumes.
|
||||
|
||||
UID/GID 10001 is the numeric edge identity. The denylist directory is mounted, rather than
|
||||
its file, so atomic replacement remains visible in the container.
|
||||
|
||||
```sh
|
||||
sudo install -d -o 10001 -g 10001 -m 0700 /var/log/truf-edge
|
||||
sudo install -d -o root -g 10001 -m 2750 /etc/truf-edge/denylist
|
||||
sudo install -m 0640 -o root -g 10001 deploy/edge/admin-denylist.caddy /etc/truf-edge/denylist/admin-denylist.caddy
|
||||
sudo install -d -o root -g root -m 0700 /var/lib/truf-edge
|
||||
sudo install -m 0750 -o root -g root deploy/fail2ban/truf_caddy_admin_denylist.py /usr/local/sbin/truf-caddy-admin-denylist
|
||||
```
|
||||
|
||||
Create `/etc/truf-edge/edge.env` as root with mode 0600. Generate a new admin segment
|
||||
with `openssl rand -hex 32`. It must be exactly 64 lowercase hex characters (256 random
|
||||
bits). Generate the bcrypt value interactively with the pinned edge image's
|
||||
`caddy hash-password` command; never put the plaintext password in a command, file, or
|
||||
Compose variable.
|
||||
|
||||
```dotenv
|
||||
TRUF_EDGE_HOST=edge.example.net
|
||||
TRUF_ADMIN_PREFIX=replace_with_64_lowercase_hex_characters
|
||||
TRUF_ADMIN_USER=operator
|
||||
TRUF_ADMIN_PASSWORD_HASH='$2a$14$replace_with_a_real_caddy_bcrypt_hash'
|
||||
TRUF_ADMIN_EDGE_MARKER=replace_with_a_second_independent_64_character_hex_secret
|
||||
TRUF_SHARED_INGRESS_MARKER=replace_with_a_third_independent_64_character_hex_secret
|
||||
TRUF_EDGE_AUTH_LOG_DIR=/var/log/truf-edge
|
||||
TRUF_EDGE_DENYLIST_DIR=/etc/truf-edge/denylist
|
||||
```
|
||||
|
||||
`TRUF_SHARED_INGRESS_MARKER` is required only by `shared-host-edge-v1`. Host Caddy strips
|
||||
any inbound transit/private headers, injects this marker and its observed client address,
|
||||
and proxies to loopback. The managed edge rejects a missing marker before trusting that
|
||||
address and removes the marker before proxying to the application.
|
||||
|
||||
Now install and validate the fixed host agent. The installer creates the fixed candidate,
|
||||
result, PostgreSQL socket, and active-document paths and enables
|
||||
`/run/truf/host-agent.sock`; Compose refuses to create missing bind sources.
|
||||
|
||||
```sh
|
||||
sudo /usr/bin/python3 -I -S -B /opt/truf/deploy/host-agent/truf_host_agent_install.py install
|
||||
sudo /usr/bin/python3 -I -S -B /opt/truf/deploy/host-agent/truf_host_agent_install.py validate
|
||||
```
|
||||
|
||||
The active `/etc/truf/runtime/config.yaml` and `secrets.yaml` are UID/GID 10001 mode 0600
|
||||
documents and are never overwritten by repeat installation. Any package manifest referenced
|
||||
by the config must be installed beneath `/etc/truf/worker-packages` as a root-owned,
|
||||
root:root mode 0644 regular file before validation. The runtime maps that immutable authority
|
||||
read-only at `/data/worker-packages`; do not place manifests in the private active-document
|
||||
directory.
|
||||
|
||||
Automatic TLS remains the default. A deployment that must use operator-provided
|
||||
certificates can mount a root-owned, non-link `*.caddy` file under `/etc/caddy/tls` and
|
||||
set `TRUF_EDGE_TLS_INCLUDE` to that absolute container path in a reviewed Compose
|
||||
override. The include should contain only the site's `tls CERT KEY` directive. Never use
|
||||
the repository's localhost test certificate or key in a deployment.
|
||||
|
||||
The normal `compose.yaml` remains private and unchanged. Confirm the host-agent socket is
|
||||
active and rerun installer validation immediately before starting production edge. Always
|
||||
supply the base file, the exact selected profile file, and the protected environment file.
|
||||
For standalone:
|
||||
|
||||
```sh
|
||||
docker compose --env-file /etc/truf-edge/edge.env -f compose.yaml -f compose.edge.yaml build runtime edge
|
||||
docker compose --env-file /etc/truf-edge/edge.env -f compose.yaml -f compose.edge.yaml up -d
|
||||
```
|
||||
|
||||
For shared host:
|
||||
|
||||
```sh
|
||||
docker compose --env-file /etc/truf-edge/edge.env -f compose.yaml -f compose.shared-host.yaml build runtime edge
|
||||
docker compose --env-file /etc/truf-edge/edge.env -f compose.yaml -f compose.shared-host.yaml up -d
|
||||
```
|
||||
|
||||
Before starting shared host, validate the complete existing host Caddy configuration with
|
||||
the snippet import in place. A matching request must fail at that Truf route if the
|
||||
loopback edge is unavailable; it must never fall through to X-UI or another upstream.
|
||||
|
||||
Provisioning creates the private `/data/managed-files` namespace in the named data volume.
|
||||
Each configured writable root must be a reviewed immediate child such as
|
||||
`/data/managed-files/exports`, created with UID/GID 10001 and mode 0700 while the runtime is
|
||||
stopped. Arbitrary host bind paths are not managed-file roots.
|
||||
|
||||
Worker admission remains disabled by `app/config.linux.yaml`. Configure the private
|
||||
runtime config's worker sources, compatibility profiles, and hashed device credentials
|
||||
before explicitly enabling `supervisor.worker_api.enabled`. The edge does not enable it.
|
||||
The typed admin backend is disabled independently under `supervisor.worker_api.admin`.
|
||||
Set its exact `origin` to `https://TRUF_EDGE_HOST`, set `edge_marker` to the same independent
|
||||
256-bit value as `TRUF_ADMIN_EDGE_MARKER`, then explicitly enable it. Both authenticated
|
||||
surfaces share the private runtime loopback port 8766. Caddy strips any inbound
|
||||
`X-Truf-Admin-Edge` and `X-Truf-Admin-Operator`, sets the configured marker and the
|
||||
authenticated Basic-auth username only after authentication, and rewrites the public
|
||||
random prefix to the private `/admin-internal` backend path. The backend accepts the
|
||||
operator identity only together with the private marker.
|
||||
Worker API requests receive neither private admin header.
|
||||
|
||||
Build and client bootstrap instructions for Windows and Linux remote workers are in
|
||||
`docs/remote-worker-operations.md`. Worker executables and images must be produced from a
|
||||
reviewed release checkout; operators must not assemble Python, Git, TruffleHog, detector
|
||||
policy, or dependencies manually on each worker.
|
||||
|
||||
## Fail2ban
|
||||
|
||||
Install the host files under their conventional names and enable fail2ban plus the expiry
|
||||
timer. The jail counts only redacted `admin_auth_failure` JSON records. An initial Basic
|
||||
challenge without credentials, Worker API authentication failures, and unrelated 404s do
|
||||
not enter that log. The action changes only the matcher imported inside the secret admin
|
||||
route; it does not create firewall rules and therefore does not block workers sharing an IP.
|
||||
|
||||
```sh
|
||||
sudo install -m 0644 deploy/fail2ban/filter.d-truf-admin-auth.conf /etc/fail2ban/filter.d/truf-admin-auth.conf
|
||||
sudo install -m 0644 deploy/fail2ban/jail.d-truf-admin-auth.local /etc/fail2ban/jail.d/truf-admin-auth.local
|
||||
sudo install -m 0644 deploy/fail2ban/action.d-truf-caddy-admin-denylist.conf /etc/fail2ban/action.d/truf-caddy-admin-denylist.conf
|
||||
sudo install -m 0644 deploy/fail2ban/fail2ban.d-truf-persistence.local /etc/fail2ban/fail2ban.d/truf-persistence.local
|
||||
sudo install -m 0644 deploy/systemd/truf-caddy-admin-denylist-expire.service /etc/systemd/system/
|
||||
sudo install -m 0644 deploy/systemd/truf-caddy-admin-denylist-expire.timer /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now fail2ban truf-caddy-admin-denylist-expire.timer
|
||||
sudo fail2ban-client status truf-admin-auth
|
||||
```
|
||||
|
||||
Fail2ban persists jail state in `/var/lib/fail2ban/fail2ban.sqlite3`. The updater persists
|
||||
canonical IPs and expiry timestamps in private
|
||||
`/var/lib/truf-edge/admin-denylist.json`. It validates the complete Caddyfile in the running
|
||||
edge container, reloads it through the private admin endpoint, and restores/reloads the
|
||||
previous state if a command fails. Reload uses Caddy's private `/run/caddy-admin.sock` inside the edge
|
||||
container. The socket is not mounted or published. Shared mode renders denylist matchers
|
||||
against the marker-authenticated client address; standalone mode uses the direct peer.
|
||||
|
||||
## SSH recovery
|
||||
|
||||
Use fail2ban's normal unban first so its database and Caddy agree:
|
||||
|
||||
```sh
|
||||
sudo fail2ban-client set truf-admin-auth unbanip 203.0.113.10
|
||||
sudo /usr/local/sbin/truf-caddy-admin-denylist status
|
||||
sudo /usr/local/sbin/truf-caddy-admin-denylist expire
|
||||
```
|
||||
|
||||
If fail2ban is unavailable, run the updater's explicit unban over SSH:
|
||||
|
||||
```sh
|
||||
sudo /usr/local/sbin/truf-caddy-admin-denylist unban 203.0.113.10
|
||||
```
|
||||
|
||||
For recovery from a damaged generated snippet, stop the expiry timer and fail2ban, restore
|
||||
`deploy/edge/admin-denylist.caddy` to `/etc/truf-edge/denylist/admin-denylist.caddy`, then
|
||||
run Caddy validation and reload through the private Unix admin socket only after validation
|
||||
succeeds. Reconcile each
|
||||
remaining address with the updater before re-enabling the services. Do not use a global
|
||||
firewall ban as a shortcut.
|
||||
Reference in New Issue
Block a user