The web management UI was a bespoke minimal theme with LDAP-bind login.
Rebuild it to match the SSO Manager and Proxy — same stack, same look/feel,
same auth model. The SSH bridge, audit, metrics, and access logic are
unchanged; this is purely the web layer.
Frontend (mirrors proxy/sso):
- Express + EJS with the shared top.ejs/bottom.ejs shell, Bootstrap 5,
jQuery, jq-repeat, FontAwesome, Socket.IO, and the shared app-base.js /
val.js client framework (copied verbatim). Vendor libs served from
node_modules via /static-modules; app assets via /static.
- Dashboard / Sessions / Audit pages render in the common look/feel,
loading data through the authenticated /api/* endpoints.
Auth (mirrors proxy):
- OIDC against the SSO (utils/oidc.js + routes/auth.js + models/oidc_state)
plus a local anti-lockout admin (models/user_redis.js, bootstrapped from
auth.adminUsers[0] / auth.localAdminPass). AuthToken sessions carry the
group snapshot; middleware gates the data API on adminGroups or the local
admin. New config: oidc{} + auth.adminUsers/localAdminPass.
- /api/user/me drives the client login state; "Log in with SSO" hidden when
oidc.enabled is false.
Verified end to end: local admin login -> token -> /api/user/me isAdmin,
metrics/sessions/audit 200 with token / 401 without / 401 bad password;
static + page shells serve; 26 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3.8 KiB
layout, title, description
| layout | title | description |
|---|---|---|
| default | Installation | Install the jump host three ways — bundled in the theta-env stack, standalone Docker, or bare metal — plus the required LDAP write-ACL and port-22 options. |
Installation
Three ways to run the jump host, in increasing manual effort. All read their
config through @simpleworkjs/conf
(conf/base.js < conf/<NODE_ENV>.js < the CONF_SECRETS file < app_* env).
Requirements
- The SSO Manager (OpenLDAP
directory +
/api/discovery), v1.3.0 or newer. - Downstream hosts joined via
ldap-client (SSSD +
AuthorizedKeysCommand). - An LDAP bind account with write access to the
sshPublicKeyattribute on user entries (see below). - An SSO API token (
sso_…) for the directory queries.
1. Unified theta-env stack (recommended)
Enable it in theta-env/setup.env:
CFG_JUMP_HOST_ENABLED=true
CFG_JUMP_HOST=jump.example.com
JUMP_SSH_PORT=2222
Re-run ./setup.sh. The stack builds the submodule (behind the jump-host
compose profile), mints the directory API token, writes
./config/jump-secrets.js, grants the sshPublicKey write-ACL, registers the
jump host in the proxy, and seeds a directory entry. Forward the public host's
:22 (or :2222) to the container's published JUMP_SSH_PORT.
2. Standalone Docker
cp secrets.js.example config/jump-secrets.js
$EDITOR config/jump-secrets.js # LDAP bind (+ sshPublicKey write ACL), SSO url + token
docker compose up -d --build
Host keys persist in the jump-data volume. The web UI is on :3002; front it
with your own TLS/proxy.
3. Bare metal
curl -fsSL https://raw.githubusercontent.com/theta42/jump-host/master/ops/install.sh | sudo bash
sudo $EDITOR /etc/jump-host/secrets.js
sudo systemctl restart jump-host
journalctl -u jump-host -f
ops/install.sh installs Node 22 + Redis, hard-resets the checkout at
/opt/theta42/jump-host to the remote branch, symlinks the systemd unit, and
runs npm ci. Idempotent — re-run to update. Overridable via REPO_DIR=,
BRANCH=, SECRETS_FILE=.
The LDAP write-ACL (required)
The jump host injects its public key into each user's sshPublicKey, so its
bind account must be able to write that attribute. In the bundled OpenLDAP:
access to attrs=sshPublicKey
by dn.exact="cn=ldapclient,ou=people,dc=example,dc=com" write
by self write
by * read
In the theta-env bundle this is handled for you (the jump host binds as the LDAP
admin). For a hardened standalone deployment, use a dedicated bind account with
exactly this attribute-scoped ACL. Without write access, key injection fails and
every bridge attempt is audited key-inject-failed.
Listening on port 22
The default SSH port is 2222 so the service needs no privilege. To listen on
22, set ssh.listenPort: 22 and either:
- systemd: uncomment
AmbientCapabilities=CAP_NET_BIND_SERVICEin the unit; - Docker: publish
22:22; or - firewall: DNAT
22 → 2222.
Configuration reference
Every key is documented in
secrets.js.example:
ldap (bind + bases + TLS), sso (url + apiToken), ssh
(listenPort, passwordAuth, allowRawIPs, keyComment, timeouts,
maxSessions), web.port, oidc (web-UI SSO login), auth
(adminGroups / adminUsers / localAdminPass), and redis.
Verifying
ssh -p 2222 youruid@jump.example.com # TUI picker
ssh -p 2222 youruid_-_somehost@jump.example.com # direct
sftp -P 2222 youruid_-_somehost@jump.example.com # WinSCP path
curl -s http://localhost:3002/health
Watch journalctl -u jump-host -f (or docker logs -f jump-host) and the audit
log at /audit in the web UI.