Initial server source import
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
{
|
||||
admin unix//run/caddy-admin.sock
|
||||
auto_https disable_redirects
|
||||
skip_install_trust
|
||||
}
|
||||
|
||||
(admin_security) {
|
||||
header {
|
||||
Cache-Control "no-store"
|
||||
Pragma "no-cache"
|
||||
Referrer-Policy "same-origin"
|
||||
Content-Security-Policy "default-src 'none'; style-src 'self'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'"
|
||||
X-Content-Type-Options "nosniff"
|
||||
}
|
||||
}
|
||||
|
||||
(admin_gate) {
|
||||
import {$TRUF_ADMIN_DENYLIST_FILE:/etc/caddy/denylist/admin-denylist.caddy}
|
||||
basic_auth bcrypt "truf-admin" {
|
||||
{$TRUF_ADMIN_USER} {$TRUF_ADMIN_PASSWORD_HASH}
|
||||
}
|
||||
}
|
||||
|
||||
{$TRUF_EDGE_HOST} {
|
||||
import {$TRUF_EDGE_TLS_INCLUDE:/etc/caddy/tls/automatic.caddy}
|
||||
import admin_security
|
||||
header Strict-Transport-Security "max-age=63072000; includeSubDomains"
|
||||
|
||||
log routine_access {
|
||||
output discard
|
||||
}
|
||||
|
||||
log admin_auth_failures {
|
||||
no_hostname
|
||||
output file {$TRUF_ADMIN_AUTH_LOG_FILE:/var/log/caddy/admin-auth-failures.json} {
|
||||
roll_size 8MiB
|
||||
roll_keep 10
|
||||
roll_keep_for 240h
|
||||
}
|
||||
format filter {
|
||||
request delete
|
||||
bytes_read delete
|
||||
user_id delete
|
||||
duration delete
|
||||
size delete
|
||||
resp_headers delete
|
||||
wrap json
|
||||
}
|
||||
}
|
||||
|
||||
@worker path /api/v1/worker/*
|
||||
handle @worker {
|
||||
reverse_proxy 127.0.0.1:8766 {
|
||||
header_up -X-Truf-Admin-Edge
|
||||
header_up -X-Truf-Admin-Operator
|
||||
header_up -Forwarded
|
||||
header_up -X-Real-IP
|
||||
}
|
||||
}
|
||||
|
||||
@admin_root path /{$TRUF_ADMIN_PREFIX}
|
||||
handle @admin_root {
|
||||
route {
|
||||
import admin_security
|
||||
import admin_gate
|
||||
redir * /{$TRUF_ADMIN_PREFIX}/ 308
|
||||
}
|
||||
}
|
||||
|
||||
@admin path /{$TRUF_ADMIN_PREFIX}/*
|
||||
handle @admin {
|
||||
route {
|
||||
import admin_security
|
||||
request_header -X-Truf-Admin-Edge
|
||||
request_header -X-Truf-Admin-Operator
|
||||
import admin_gate
|
||||
uri strip_prefix /{$TRUF_ADMIN_PREFIX}
|
||||
uri path_regexp ^ /admin-internal
|
||||
reverse_proxy 127.0.0.1:8766 {
|
||||
header_up -Authorization
|
||||
header_up X-Truf-Admin-Edge {$TRUF_ADMIN_EDGE_MARKER}
|
||||
header_up X-Truf-Admin-Operator {http.auth.user.id}
|
||||
header_up -Forwarded
|
||||
header_up -X-Real-IP
|
||||
header_down -Strict-Transport-Security
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handle {
|
||||
respond "" 404
|
||||
}
|
||||
|
||||
handle_errors {
|
||||
@bad_admin_credentials {
|
||||
path /{$TRUF_ADMIN_PREFIX} /{$TRUF_ADMIN_PREFIX}/*
|
||||
header Authorization *
|
||||
expression {err.status_code} == 401
|
||||
}
|
||||
handle @bad_admin_credentials {
|
||||
route {
|
||||
import admin_security
|
||||
log_name admin_auth_failures
|
||||
log_append event admin_auth_failure
|
||||
log_append remote_ip {http.request.remote.host}
|
||||
header WWW-Authenticate "Basic realm=\"truf-admin\""
|
||||
respond "" 401
|
||||
}
|
||||
}
|
||||
|
||||
@admin_unauthorized {
|
||||
path /{$TRUF_ADMIN_PREFIX} /{$TRUF_ADMIN_PREFIX}/*
|
||||
expression {err.status_code} == 401
|
||||
}
|
||||
handle @admin_unauthorized {
|
||||
route {
|
||||
import admin_security
|
||||
header WWW-Authenticate "Basic realm=\"truf-admin\""
|
||||
respond "" 401
|
||||
}
|
||||
}
|
||||
|
||||
@admin_error path /{$TRUF_ADMIN_PREFIX} /{$TRUF_ADMIN_PREFIX}/*
|
||||
handle @admin_error {
|
||||
import admin_security
|
||||
respond "" {err.status_code}
|
||||
}
|
||||
|
||||
handle {
|
||||
respond "" {err.status_code}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
{
|
||||
admin unix//run/caddy-admin.sock
|
||||
auto_https off
|
||||
skip_install_trust
|
||||
servers {
|
||||
trusted_proxies static 127.0.0.1/32 ::1/128
|
||||
trusted_proxies_strict
|
||||
client_ip_headers X-Forwarded-For
|
||||
}
|
||||
}
|
||||
|
||||
(admin_security) {
|
||||
header {
|
||||
Cache-Control "no-store"
|
||||
Pragma "no-cache"
|
||||
Referrer-Policy "same-origin"
|
||||
Content-Security-Policy "default-src 'none'; style-src 'self'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'"
|
||||
X-Content-Type-Options "nosniff"
|
||||
}
|
||||
}
|
||||
|
||||
(admin_gate) {
|
||||
import {$TRUF_ADMIN_DENYLIST_FILE:/etc/caddy/denylist/admin-denylist.caddy}
|
||||
basic_auth bcrypt "truf-admin" {
|
||||
{$TRUF_ADMIN_USER} {$TRUF_ADMIN_PASSWORD_HASH}
|
||||
}
|
||||
}
|
||||
|
||||
http://:18766 {
|
||||
bind 127.0.0.1
|
||||
|
||||
log routine_access {
|
||||
output discard
|
||||
}
|
||||
|
||||
log admin_auth_failures {
|
||||
no_hostname
|
||||
output file {$TRUF_ADMIN_AUTH_LOG_FILE:/var/log/caddy/admin-auth-failures.json} {
|
||||
roll_size 8MiB
|
||||
roll_keep 10
|
||||
roll_keep_for 240h
|
||||
}
|
||||
format filter {
|
||||
request delete
|
||||
bytes_read delete
|
||||
user_id delete
|
||||
duration delete
|
||||
size delete
|
||||
resp_headers delete
|
||||
wrap json
|
||||
}
|
||||
}
|
||||
|
||||
route {
|
||||
@invalid_ingress not header X-Truf-Shared-Ingress {$TRUF_SHARED_INGRESS_MARKER}
|
||||
respond @invalid_ingress "" 403
|
||||
request_header -X-Truf-Shared-Ingress
|
||||
|
||||
import admin_security
|
||||
header Strict-Transport-Security "max-age=63072000; includeSubDomains"
|
||||
|
||||
@worker path /api/v1/worker/*
|
||||
handle @worker {
|
||||
reverse_proxy 127.0.0.1:8766 {
|
||||
header_up -X-Truf-Admin-Edge
|
||||
header_up -X-Truf-Admin-Operator
|
||||
header_up -X-Truf-Shared-Ingress
|
||||
header_up -Forwarded
|
||||
header_up -X-Real-IP
|
||||
}
|
||||
}
|
||||
|
||||
@admin_root path /{$TRUF_ADMIN_PREFIX}
|
||||
handle @admin_root {
|
||||
route {
|
||||
import admin_security
|
||||
import admin_gate
|
||||
redir * /{$TRUF_ADMIN_PREFIX}/ 308
|
||||
}
|
||||
}
|
||||
|
||||
@admin path /{$TRUF_ADMIN_PREFIX}/*
|
||||
handle @admin {
|
||||
route {
|
||||
import admin_security
|
||||
request_header -X-Truf-Admin-Edge
|
||||
request_header -X-Truf-Admin-Operator
|
||||
import admin_gate
|
||||
uri strip_prefix /{$TRUF_ADMIN_PREFIX}
|
||||
uri path_regexp ^ /admin-internal
|
||||
reverse_proxy 127.0.0.1:8766 {
|
||||
header_up -Authorization
|
||||
header_up X-Truf-Admin-Edge {$TRUF_ADMIN_EDGE_MARKER}
|
||||
header_up X-Truf-Admin-Operator {http.auth.user.id}
|
||||
header_up -X-Truf-Shared-Ingress
|
||||
header_up -Forwarded
|
||||
header_up -X-Real-IP
|
||||
header_down -Strict-Transport-Security
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handle {
|
||||
respond "" 404
|
||||
}
|
||||
}
|
||||
|
||||
handle_errors {
|
||||
@bad_admin_credentials {
|
||||
path /{$TRUF_ADMIN_PREFIX} /{$TRUF_ADMIN_PREFIX}/*
|
||||
header Authorization *
|
||||
expression {err.status_code} == 401
|
||||
}
|
||||
handle @bad_admin_credentials {
|
||||
route {
|
||||
import admin_security
|
||||
log_name admin_auth_failures
|
||||
log_append event admin_auth_failure
|
||||
log_append remote_ip {http.request.client_ip}
|
||||
header WWW-Authenticate "Basic realm=\"truf-admin\""
|
||||
respond "" 401
|
||||
}
|
||||
}
|
||||
|
||||
@admin_unauthorized {
|
||||
path /{$TRUF_ADMIN_PREFIX} /{$TRUF_ADMIN_PREFIX}/*
|
||||
expression {err.status_code} == 401
|
||||
}
|
||||
handle @admin_unauthorized {
|
||||
route {
|
||||
import admin_security
|
||||
header WWW-Authenticate "Basic realm=\"truf-admin\""
|
||||
respond "" 401
|
||||
}
|
||||
}
|
||||
|
||||
@admin_error path /{$TRUF_ADMIN_PREFIX} /{$TRUF_ADMIN_PREFIX}/*
|
||||
handle @admin_error {
|
||||
import admin_security
|
||||
respond "" {err.status_code}
|
||||
}
|
||||
|
||||
handle {
|
||||
respond "" {err.status_code}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
FROM caddy:2.10.2-alpine@sha256:4c6e91c6ed0e2fa03efd5b44747b625fec79bc9cd06ac5235a779726618e530d AS edge
|
||||
|
||||
USER 0:0
|
||||
RUN setcap -r /usr/bin/caddy \
|
||||
&& install -d -o 10001 -g 10001 -m 0700 /data /config /var/log/caddy /etc/caddy/denylist /etc/caddy/tls
|
||||
COPY --chown=0:0 --chmod=0444 deploy/edge/Caddyfile /etc/caddy/Caddyfile
|
||||
COPY --chown=0:0 --chmod=0444 deploy/edge/Caddyfile.shared-host /etc/caddy/Caddyfile.shared-host
|
||||
COPY --chown=0:0 --chmod=0444 deploy/edge/admin-denylist.caddy /etc/caddy/denylist/admin-denylist.caddy
|
||||
COPY --chown=0:0 --chmod=0444 deploy/edge/automatic-tls.caddy /etc/caddy/tls/automatic.caddy
|
||||
COPY --chown=0:0 --chmod=0555 deploy/edge/entrypoint.sh /usr/local/bin/truf-edge-entrypoint
|
||||
|
||||
USER 10001:10001
|
||||
ENTRYPOINT ["/usr/local/bin/truf-edge-entrypoint"]
|
||||
CMD []
|
||||
|
||||
FROM edge AS edge-e2e
|
||||
|
||||
USER 0:0
|
||||
RUN chmod 0644 /etc/caddy/Caddyfile \
|
||||
&& sed -i 's#^[[:space:]]*admin unix//run/caddy-admin.sock$#\tadmin 127.0.0.1:2019#' /etc/caddy/Caddyfile \
|
||||
&& chmod 0444 /etc/caddy/Caddyfile \
|
||||
&& grep -Fx ' admin 127.0.0.1:2019' /etc/caddy/Caddyfile >/dev/null
|
||||
USER 10001:10001
|
||||
@@ -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.
|
||||
@@ -0,0 +1 @@
|
||||
# Managed by truf-caddy-admin-denylist. Admin-route import only.
|
||||
@@ -0,0 +1 @@
|
||||
# Empty by design: Caddy's automatic TLS remains the production default.
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
fail() {
|
||||
echo "edge configuration rejected: $1" >&2
|
||||
exit 64
|
||||
}
|
||||
|
||||
host=${TRUF_EDGE_HOST:-}
|
||||
prefix=${TRUF_ADMIN_PREFIX:-}
|
||||
user=${TRUF_ADMIN_USER:-}
|
||||
password_hash=${TRUF_ADMIN_PASSWORD_HASH:-}
|
||||
edge_marker=${TRUF_ADMIN_EDGE_MARKER:-}
|
||||
edge_mode=${TRUF_EDGE_MODE:-standalone-edge-v1}
|
||||
ingress_marker=${TRUF_SHARED_INGRESS_MARKER:-}
|
||||
tls_include=${TRUF_EDGE_TLS_INCLUDE:-}
|
||||
|
||||
case "$edge_mode" in
|
||||
standalone-edge-v1) caddyfile=/etc/caddy/Caddyfile ;;
|
||||
shared-host-edge-v1)
|
||||
caddyfile=/etc/caddy/Caddyfile.shared-host
|
||||
[ "${#ingress_marker}" -eq 64 ] || fail "TRUF_SHARED_INGRESS_MARKER must encode 256 random bits"
|
||||
printf '%s' "$ingress_marker" | grep -Eq '^[0-9a-f]{64}$' \
|
||||
|| fail "TRUF_SHARED_INGRESS_MARKER must encode 256 random bits"
|
||||
;;
|
||||
*) fail "TRUF_EDGE_MODE is unsupported" ;;
|
||||
esac
|
||||
|
||||
if [ "$host" = localhost ]; then
|
||||
[ -n "$tls_include" ] || fail "localhost requires an explicit static TLS include"
|
||||
else
|
||||
case "$host" in
|
||||
''|*://*|*/*|*:*|.*|*..*|*.) fail "TRUF_EDGE_HOST must be one DNS hostname" ;;
|
||||
esac
|
||||
printf '%s' "$host" | awk -F. '
|
||||
length($0) > 253 || NF < 2 { exit 1 }
|
||||
{ for (i = 1; i <= NF; i++) if (length($i) > 63 || $i !~ /^[A-Za-z0-9-]+$/ || $i ~ /^-/ || $i ~ /-$/) exit 1 }
|
||||
' \
|
||||
|| fail "TRUF_EDGE_HOST must be one DNS hostname"
|
||||
fi
|
||||
|
||||
if [ -n "$tls_include" ]; then
|
||||
case "$tls_include" in
|
||||
/etc/caddy/tls/*.caddy) ;;
|
||||
*) fail "TRUF_EDGE_TLS_INCLUDE must be an absolute Caddy TLS include" ;;
|
||||
esac
|
||||
[ -f "$tls_include" ] && [ ! -L "$tls_include" ] \
|
||||
|| fail "TRUF_EDGE_TLS_INCLUDE must be a regular non-link file"
|
||||
fi
|
||||
|
||||
[ "${#prefix}" -eq 64 ] || fail "TRUF_ADMIN_PREFIX must encode 256 random bits"
|
||||
printf '%s' "$prefix" | grep -Eq '^[0-9a-f]{64}$' \
|
||||
|| fail "TRUF_ADMIN_PREFIX must encode 256 random bits"
|
||||
|
||||
printf '%s' "$user" | grep -Eq '^[A-Za-z0-9_.-]{1,64}$' \
|
||||
|| fail "TRUF_ADMIN_USER has an unsupported form"
|
||||
printf '%s' "$password_hash" | grep -Eq '^\$2[aby]\$(0[4-9]|[12][0-9]|3[01])\$[./A-Za-z0-9]{53}$' \
|
||||
|| fail "TRUF_ADMIN_PASSWORD_HASH must be a supported bcrypt hash"
|
||||
[ "${#edge_marker}" -eq 64 ] || fail "TRUF_ADMIN_EDGE_MARKER must encode 256 random bits"
|
||||
printf '%s' "$edge_marker" | grep -Eq '^[0-9a-f]{64}$' \
|
||||
|| fail "TRUF_ADMIN_EDGE_MARKER must encode 256 random bits"
|
||||
|
||||
[ -f /etc/caddy/denylist/admin-denylist.caddy ] \
|
||||
|| fail "the managed admin denylist snippet is missing"
|
||||
[ ! -L /etc/caddy/denylist/admin-denylist.caddy ] \
|
||||
|| fail "the managed admin denylist snippet must not be a link"
|
||||
[ -d /var/log/caddy ] && [ -w /var/log/caddy ] \
|
||||
|| fail "the authentication log directory is not writable"
|
||||
|
||||
umask 077
|
||||
exec caddy run --config "$caddyfile" --adapter caddyfile
|
||||
@@ -0,0 +1,26 @@
|
||||
# Import this route-only snippet inside the reviewed public site block.
|
||||
@truf_worker path /api/v1/worker/*
|
||||
handle @truf_worker {
|
||||
reverse_proxy 127.0.0.1:18766 {
|
||||
header_up X-Truf-Shared-Ingress {$TRUF_SHARED_INGRESS_MARKER}
|
||||
header_up -X-Truf-Admin-Edge
|
||||
header_up -X-Truf-Admin-Operator
|
||||
header_up -Forwarded
|
||||
header_up -X-Real-IP
|
||||
header_up -X-Forwarded-For
|
||||
header_up X-Forwarded-For {http.request.remote.host}
|
||||
}
|
||||
}
|
||||
|
||||
@truf_admin path /{$TRUF_ADMIN_PREFIX} /{$TRUF_ADMIN_PREFIX}/*
|
||||
handle @truf_admin {
|
||||
reverse_proxy 127.0.0.1:18766 {
|
||||
header_up X-Truf-Shared-Ingress {$TRUF_SHARED_INGRESS_MARKER}
|
||||
header_up -X-Truf-Admin-Edge
|
||||
header_up -X-Truf-Admin-Operator
|
||||
header_up -Forwarded
|
||||
header_up -X-Real-IP
|
||||
header_up -X-Forwarded-For
|
||||
header_up X-Forwarded-For {http.request.remote.host}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user