feat: implement Remote WinBox worker, API, frontend integration, OpenBao persistence, and supporting docs

This commit is contained in:
Jason Staack
2026-03-14 09:05:14 -05:00
parent 7af08276ea
commit 970501e453
86 changed files with 3440 additions and 3764 deletions

View File

@@ -0,0 +1,71 @@
# The Other Dude — Apache reverse proxy example
#
# Required modules:
# a2enmod proxy proxy_http proxy_wstunnel rewrite ssl headers
#
# This config assumes:
# - TOD frontend runs on FRONTEND_HOST:3000
# - TOD API runs on API_HOST:8001
# - WinBox worker Xpra ports are on WORKER_HOST:10100-10119
#
# Replace tod.example.com and upstream addresses with your values.
<VirtualHost *:80>
ServerName tod.example.com
RewriteEngine On
RewriteRule ^(.*)$ https://%{HTTP_HOST}$1 [R=301,L]
</VirtualHost>
<VirtualHost *:443>
ServerName tod.example.com
SSLEngine on
SSLCertificateFile /etc/ssl/certs/tod.example.com.pem
SSLCertificateKeyFile /etc/ssl/private/tod.example.com.key
# ── Security headers ──────────────────────────────────────────────
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
# ── Xpra (Remote WinBox) ─────────────────────────────────────────
# Must appear BEFORE the general proxy rules.
# WebSocket upgrade is required. Do NOT enable mod_deflate on this path
# — compressing WebSocket binary frames corrupts Xpra mouse/keyboard data.
#
# ProxyPassMatch uses regex to capture the port and forward to the worker.
# Ports 10100-10119 (up to 20 concurrent sessions).
RewriteEngine On
# WebSocket upgrade for Xpra
RewriteCond %{HTTP:Upgrade} =websocket [NC]
RewriteRule ^/xpra/(\d+)/(.*)$ ws://YOUR_TOD_HOST:$1/$2 [P,L]
# Regular HTTP requests for Xpra HTML5 client assets
ProxyPassMatch "^/xpra/(\d+)/(.*)" "http://YOUR_TOD_HOST:$1/$2"
# Relaxed CSP for Xpra HTML5 client (inline scripts + eval)
<LocationMatch "^/xpra/">
Header always set Content-Security-Policy "default-src 'self' 'unsafe-inline' 'unsafe-eval' ws: wss: data: blob:; frame-ancestors 'self';"
SetEnv no-gzip 1
</LocationMatch>
# ── API ───────────────────────────────────────────────────────────
ProxyPass /api/ http://YOUR_TOD_HOST:8001/api/
ProxyPassReverse /api/ http://YOUR_TOD_HOST:8001/api/
ProxyTimeout 300
RequestHeader set X-Forwarded-Proto "https"
<Location /api/>
# Let the API set its own CSP
Header unset Content-Security-Policy
</Location>
# ── Frontend (SPA) ────────────────────────────────────────────────
ProxyPass / http://YOUR_TOD_HOST:3000/
ProxyPassReverse / http://YOUR_TOD_HOST:3000/
ProxyPreserveHost On
</VirtualHost>

View File

@@ -0,0 +1,71 @@
# The Other Dude — Caddy reverse proxy example
#
# This config assumes:
# - TOD frontend runs on FRONTEND_HOST:3000
# - TOD API runs on API_HOST:8001
# - WinBox worker Xpra ports are on WORKER_HOST:10100-10119
#
# Replace tod.example.com and the upstream IPs with your values.
# Caddy handles TLS automatically via Let's Encrypt.
tod.example.com {
log {
output file /var/log/caddy/tod.log {
roll_size 50mb
roll_keep 5
}
format json
}
encode zstd gzip
header {
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
X-XSS-Protection "1; mode=block"
Referrer-Policy "strict-origin-when-cross-origin"
-Server
}
# ── Xpra (Remote WinBox) ──────────────────────────────────────────
# Proxies the Xpra HTML5 client to winbox-worker Xpra ports.
# Port range 10100-10119 (up to 20 concurrent sessions).
# Uses scoped compression to avoid corrupting WebSocket binary frames.
@xpra path_regexp xpra ^/xpra/(101[0-1][0-9])/(.*)$
handle @xpra {
# Override parent encode — only compress text assets, NOT WebSocket frames
encode {
gzip
match {
header Content-Type text/*
header Content-Type application/javascript*
header Content-Type application/json*
}
}
uri strip_prefix /xpra/{re.xpra.1}
reverse_proxy {$WORKER_HOST:YOUR_TOD_HOST}:{re.xpra.1} {
header_up Host {host}
header_up X-Real-IP {remote_host}
}
}
# ── API ───────────────────────────────────────────────────────────
handle /api/* {
reverse_proxy http://{$API_HOST:YOUR_TOD_HOST}:8001 {
header_up Host {host}
header_up X-Real-IP {remote_host}
transport http {
dial_timeout 30s
response_header_timeout 60s
}
}
}
# ── Frontend (SPA) ────────────────────────────────────────────────
handle {
reverse_proxy http://{$FRONTEND_HOST:YOUR_TOD_HOST}:3000 {
header_up Host {host}
header_up X-Real-IP {remote_host}
}
}
}

View File

@@ -0,0 +1,77 @@
# The Other Dude — HAProxy reverse proxy example
#
# This config assumes:
# - TOD frontend runs on FRONTEND_HOST:3000
# - TOD API runs on API_HOST:8001
# - WinBox worker Xpra ports are on WORKER_HOST:10100-10119
# - TLS is terminated by HAProxy
#
# Replace tod.example.com and upstream addresses with your values.
#
# IMPORTANT: Do NOT enable compression on the xpra backend —
# compressing WebSocket binary frames corrupts Xpra mouse/keyboard data.
global
log stdout format raw local0
maxconn 4096
defaults
log global
mode http
option httplog
timeout connect 10s
timeout client 300s
timeout server 300s
timeout tunnel 3600s
# ── Frontend ─────────────────────────────────────────────────────────
frontend https
bind *:443 ssl crt /etc/ssl/certs/tod.example.com.pem
bind *:80
redirect scheme https code 301 if !{ ssl_fc }
# Security headers
http-response set-header X-Frame-Options "SAMEORIGIN"
http-response set-header X-Content-Type-Options "nosniff"
http-response set-header Referrer-Policy "strict-origin-when-cross-origin"
# Routing rules (order matters — first match wins)
acl is_xpra path_beg /xpra/
acl is_api path_beg /api/
use_backend xpra if is_xpra
use_backend api if is_api
default_backend frontend
# ── Backends ─────────────────────────────────────────────────────────
backend api
option forwardfor
http-request set-header X-Forwarded-Proto https
server api1 YOUR_TOD_HOST:8001 check
backend frontend
option forwardfor
server fe1 YOUR_TOD_HOST:3000 check
# Xpra backend — uses a Lua or map-based approach to extract the port
# from the URL path. This example covers port 10100; add servers for
# 10101-10119 as needed, or use HAProxy's Lua scripting for dynamic routing.
#
# WARNING: Do NOT add "compression" directives to this backend.
backend xpra
option forwardfor
# Strip /xpra/{port} prefix
http-request set-path %[path,regsub(^/xpra/[0-9]+/,/)]
# Route to the correct port based on URL
# For dynamic port routing, use a map file or Lua script.
# Static example for port 10100:
acl xpra_10100 path_beg /xpra/10100/
use-server xpra10100 if xpra_10100
server xpra10100 YOUR_TOD_HOST:10100 check
# server xpra10101 YOUR_TOD_HOST:10101 check
# ... add through 10119 as needed

View File

@@ -0,0 +1,90 @@
# The Other Dude — nginx reverse proxy example
#
# This config assumes:
# - TOD frontend runs on FRONTEND_HOST:3000
# - TOD API runs on API_HOST:8001
# - WinBox worker Xpra ports are on WORKER_HOST:10100-10119
# - TLS is terminated by nginx (or upstream load balancer)
#
# Replace tod.example.com and upstream addresses with your values.
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream tod_frontend {
server YOUR_TOD_HOST:3000;
}
upstream tod_api {
server YOUR_TOD_HOST:8001;
}
server {
listen 80;
server_name tod.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name tod.example.com;
ssl_certificate /etc/ssl/certs/tod.example.com.pem;
ssl_certificate_key /etc/ssl/private/tod.example.com.key;
# ── Security headers ──────────────────────────────────────────────
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://*.tile.openstreetmap.org; font-src 'self'; connect-src 'self' ws: wss:; worker-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self';" always;
# ── API ───────────────────────────────────────────────────────────
location /api/ {
proxy_pass http://tod_api;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_read_timeout 300s;
proxy_hide_header Content-Security-Policy;
}
# ── Xpra (Remote WinBox) ─────────────────────────────────────────
# Proxies Xpra HTML5 client to winbox-worker ports 10100-10119.
# WebSocket support is required. Do NOT enable gzip on this location
# — compressing WebSocket binary frames corrupts Xpra mouse/keyboard data.
location ~ ^/xpra/(\d+)/(.*) {
set $xpra_port $1;
set $xpra_path $2;
proxy_pass http://YOUR_TOD_HOST:$xpra_port/$xpra_path$is_args$args;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 300s;
proxy_buffering off;
# Xpra HTML5 client needs relaxed CSP (inline scripts + eval)
# Adding add_header in a location block replaces all server-level headers in nginx
add_header Content-Security-Policy "default-src 'self' 'unsafe-inline' 'unsafe-eval' ws: wss: data: blob:; frame-ancestors 'self';" always;
add_header X-Content-Type-Options "nosniff" always;
}
# ── Frontend (SPA) ────────────────────────────────────────────────
location / {
proxy_pass http://tod_frontend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

View File

@@ -0,0 +1,93 @@
# The Other Dude — Traefik dynamic configuration example
#
# This config assumes:
# - TOD frontend runs on FRONTEND_HOST:3000
# - TOD API runs on API_HOST:8001
# - WinBox worker Xpra ports are on WORKER_HOST:10100-10119
# - Traefik entrypoints: web (80) and websecure (443)
#
# Replace tod.example.com and upstream addresses with your values.
#
# For Docker-based Traefik, labels can replace this file.
# This example uses file provider for clarity.
http:
routers:
# ── Xpra (Remote WinBox) ────────────────────────────────────────
# Must be higher priority than the frontend catch-all.
# Each Xpra port needs its own service since Traefik doesn't
# support dynamic port extraction from path regex.
# Shown for port 10100; duplicate for 10101-10119 as needed.
tod-xpra-10100:
rule: "Host(`tod.example.com`) && PathPrefix(`/xpra/10100/`)"
entryPoints: [websecure]
service: tod-xpra-10100
middlewares: [xpra-strip, xpra-headers]
tls:
certResolver: letsencrypt
priority: 30
# ── API ─────────────────────────────────────────────────────────
tod-api:
rule: "Host(`tod.example.com`) && PathPrefix(`/api/`)"
entryPoints: [websecure]
service: tod-api
middlewares: [security-headers]
tls:
certResolver: letsencrypt
priority: 20
# ── Frontend (SPA) ──────────────────────────────────────────────
tod-frontend:
rule: "Host(`tod.example.com`)"
entryPoints: [websecure]
service: tod-frontend
middlewares: [security-headers]
tls:
certResolver: letsencrypt
priority: 10
services:
tod-xpra-10100:
loadBalancer:
servers:
- url: "http://YOUR_TOD_HOST:10100"
# Add tod-xpra-10101 through tod-xpra-10119 as needed
tod-api:
loadBalancer:
servers:
- url: "http://YOUR_TOD_HOST:8001"
tod-frontend:
loadBalancer:
servers:
- url: "http://YOUR_TOD_HOST:3000"
middlewares:
xpra-strip:
# Strip /xpra/{port} prefix before forwarding
stripPrefixRegex:
regex: ["^/xpra/[0-9]+"]
xpra-headers:
headers:
# Relaxed CSP for Xpra HTML5 client (inline scripts + eval)
customResponseHeaders:
Content-Security-Policy: "default-src 'self' 'unsafe-inline' 'unsafe-eval' ws: wss: data: blob:; frame-ancestors 'self';"
X-Content-Type-Options: "nosniff"
# IMPORTANT: Disable compression for Xpra — compressing WebSocket
# binary frames corrupts mouse/keyboard coordinate data.
security-headers:
headers:
frameDeny: true
contentTypeNosniff: true
browserXssFilter: true
referrerPolicy: "strict-origin-when-cross-origin"
customResponseHeaders:
X-Frame-Options: "SAMEORIGIN"
# IMPORTANT: Disable Traefik's built-in compression for Xpra routes.
# If using --entrypoints.websecure.http.middlewares=compress@...,
# exclude the xpra router or WebSocket binary frames will be corrupted.