wmantly 111f5d9df7 feat(mesh): UI for the gateway-to-gateway mesh
The mesh API (routes/mesh.js) had zero UI -- minting a join token,
joining a remote gateway, or seeing what's meshed all required calling
the API directly. New Mesh page (nav: Dashboard/Sessions/WireGuard/
Mesh/Audit):

- This Gateway card: interface name, kernel-vs-userspace WireGuard mode
  (wireguard-go fallback), meshed-gateway count.
- Mint a Join Token: calls POST /api/mesh/join-tokens, shows the
  single-use token once.
- Join a Remote Gateway's Mesh: calls POST /api/mesh/join with a remote
  endpoint + token.
- Meshed Gateways table: site, mesh index, mesh subnet, endpoint, public
  key, last seen -- including this gateway's own self-entry.

EJS compile verified; jump-host's existing test suite (34 tests) still
passes. Not yet visually driven in a browser the way sso-manager-node's
modal was (jump-host's OIDC-based admin auth is a heavier lift to stand
up for a one-off check) -- route registration, EJS compilation, and the
API layer underneath are verified; the actual click-through is not.
2026-08-10 18:31:03 -04:00

Theta Gateway

An SSH jump gateway and integrated WireGuard mesh network router for the Theta Suite ecosystem. Users SSH into one entry point (:2222) and reach target downstream hosts according to directory permissions, with full auditing end-to-end.

Theta Gateway authenticates users against the shared OpenLDAP directory, authorizes access using Theta Directory (theta-directory), and routes cross-site mesh traffic with native WireGuard subnets and NETMAP shadow network support.

Documentation: https://theta42.github.io/theta-suite/jump-host/

Access Flow

Direct (WinSCP/SFTP-friendly):

ssh alice_-_web01@jump.example.com          # -> target host slug 'web01'
sftp -P 2222 alice_-_web01@jump.example.com # SFTP passes through unchanged

The username grammar is {uid}_-_{target}. target is a directory host slug or hostname.

Interactive host picker:

ssh alice@jump.example.com

Plain login displays a TUI list of target hosts assigned to the local site (SITE_SLUG) that the user is authorized to reach.

How it Works

  1. Inbound Auth — OpenLDAP authentication via public key matching (sshPublicKey) or LDAP password bind.
  2. Authorization — Calls Theta Directory's access API (GET /api/discovery/access/:uid) to evaluate LDAP group memberships and site-filtered host entitlement.
  3. Key Injection — Appends its gateway public key to user sshPublicKey in LDAP and connects downstream as the user.
  4. Bridge & Audit — Slices shell/SFTP subsystem to downstream sshd with session audit logging.

Deployment

Theta Gateway is deployed exclusively via Docker Compose as an integrated service within Theta Suite — it is not installed or run on its own:

git clone --recursive https://github.com/theta42/theta-suite.git
cd theta-suite
cp setup.env.example setup.env   # set CFG_DOMAIN to your domain
./setup.sh                       # generates config, builds, and starts Theta Suite

Enable it via CFG_JUMP_HOST_ENABLED=true in setup.env and re-run ./setup.sh. The stack wires the LDAP bind account (write access to the sshPublicKey attribute), the write-ACL, the SSO API token, and a directory entry automatically.

See the main Theta Suite README for full details on multi-site configuration, WireGuard mesh routing, and network setup.

Ports

Port Purpose
2222 SSH front door (default; see below for :22)
3002 Web UI + HTTP API (audit, metrics)

The default SSH port is 2222 so the service needs no privilege. To listen on 22, set ssh.listenPort: 22 in your secrets and either uncomment AmbientCapabilities=CAP_NET_BIND_SERVICE in the systemd unit, or DNAT 22 → 2222 at the firewall.

Web UI / API

https://jump.example.com/ (behind the proxy) — built on the same Express + EJS + Bootstrap stack as Theta Directory and Theta Proxy, so it looks and behaves like the rest of the stack. Login is OIDC against the SSO (the "Log in with SSO" button) plus a local anti-lockout admin that works even if the SSO is unreachable. Admin access requires membership in auth.adminGroups (default app_sso_admin) or being the local auth.adminUsers account.

  • GET /health — open; {status, activeSessions, version}
  • GET /api/sessions — active sessions
  • GET /api/audit?page=&uid=&target=&status= — paged audit log
  • GET /api/metrics — counters (total, failures, top users/hosts)

Configuration

Config layers via @simpleworkjs/conf: conf/base.js < conf/<NODE_ENV>.js < the CONF_SECRETS file < app_* env. See secrets.js.example for every key.

Secrets

At boot, @simpleworkjs/bao-conf deep-merges secret/jump-host/conf from OpenBao over the file-loaded config. The jump host's OIDC clientSecret is captured at require time (inside createOidcClient during require('../models')), so bin/www runs bao-conf.init() before require('../models'). Fail-soft: if OpenBao is unreachable, boot continues from CONF_SECRETS. The jump host authenticates to OpenBao with the scoped VAULT_TOKEN (env, policy jump-host — read only secret/jump-host/conf), never the root token.

The config/jump-secrets.js file is an operator-edit seed artifact (gitignored); the bootstrap writes the generated API token + OAuth client into OpenBao, which is authoritative. For the full architecture see theta-suite's Secrets docs.

Development

cd nodejs && npm install
npm test          # unit + integration (node --test)
NODE_ENV=development npm run dev

License

MIT

S
Description
SSH jump host for the theta42 stack — LDAP-authenticated, directory-driven host bridging with audit and metrics
Readme MIT 1.2 MiB
Languages
JavaScript 72.9%
EJS 23.9%
Shell 2.1%
Dockerfile 0.8%
CSS 0.3%