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:
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:
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:
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:
- Install the reviewed Linux, Docker Engine and Compose plugin, systemd, Python 3, and fail2ban prerequisites; provision DNS and the selected TLS ownership boundary.
- Install the release checkout root-owned at
/opt/trufand choose exactly one deployment profile before installing the host agent. - 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.
- 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.
- Install and validate the fixed host agent. Its first install seeds an absent active
config from
app/config.linux.yamland an absent secrets document as an empty mapping; repeat installation never replaces active documents. - Install every trusted worker-package manifest referenced by the runtime config beneath
/etc/truf/worker-packageswith the exact ownership and mode described below. - Review the private active config and secrets, then build
runtimeandedgefrom the same checkout with the exact base and selected profile Compose files. - 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.
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.
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.
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:
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:
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.
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:
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:
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.