feat: initial jump-host — SSH jump host for the theta42 stack

An SSH jump host that authenticates users against the shared LDAP
directory, authorizes them from the SSO Manager's inventory graph, and
bridges them to downstream hosts — auditing everything.

- Username-grammar routing (uid_-_target@jump) + interactive TUI picker
- Inbound LDAP auth (publickey / password with off|local|all policy)
- Directory-driven access (LDAP groups x /api/discovery/resources?group=)
- Per-user key injection into sshPublicKey, connects downstream as the user
- Shell / exec / SFTP-subsystem bridging (WinSCP works)
- Web UI + HTTP API (:3002) for audit + metrics; LDAP-admin gated
- Packaged like proxy: ops/install.sh + systemd, all-in-one Docker, compose
- Tests: 23 unit + 3 integration (node --test), all green

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 15:46:19 -04:00
commit 36e9d5b0b3
51 changed files with 4291 additions and 0 deletions
+39
View File
@@ -0,0 +1,39 @@
<%- include('top') %>
<h1>Audit log</h1>
<form class="filters" method="get">
<input name="uid" placeholder="user" value="<%= query.uid || '' %>">
<input name="target" placeholder="target" value="<%= query.target || '' %>">
<select name="status">
<option value="">any</option>
<option value="success" <%= query.status === 'success' ? 'selected' : '' %>>success</option>
<option value="fail" <%= query.status === 'fail' ? 'selected' : '' %>>fail</option>
</select>
<button>Filter</button>
</form>
<table>
<thead><tr><th>Time</th><th>User</th><th>Method</th><th>Mode</th><th>Target</th><th>Chan</th><th>Client</th><th>Result</th><th>Bytes</th></tr></thead>
<tbody>
<% data.results.forEach(e => { %>
<tr class="<%= e.success ? '' : 'bad' %>">
<td><%= new Date(e.ts).toLocaleString() %></td>
<td><%= e.uid %></td>
<td><%= e.authMethod %></td>
<td><%= e.mode %></td>
<td><%= e.targetSlug || e.targetAddr || '—' %></td>
<td><%= e.channel || '—' %></td>
<td><%= e.clientIp %></td>
<td><%= e.success ? '✓' : '✗ ' + e.failReason %></td>
<td class="r"><%= (e.bytesIn + e.bytesOut) || 0 %></td>
</tr>
<% }) %>
</tbody>
</table>
<div class="pager">
<% const p = data.page; %>
<% if (p > 0) { %><a href="?page=<%= p-1 %>&uid=<%= query.uid||'' %>&target=<%= query.target||'' %>&status=<%= query.status||'' %>">← prev</a><% } %>
<span><%= data.total %> events</span>
<% if ((p+1) * data.pageSize < data.total) { %><a href="?page=<%= p+1 %>&uid=<%= query.uid||'' %>&target=<%= query.target||'' %>&status=<%= query.status||'' %>">next →</a><% } %>
</div>
<%- include('bottom') %>
+6
View File
@@ -0,0 +1,6 @@
</main>
<footer class="foot">
<% if (typeof buildInfo !== 'undefined') { %><span>v<%= buildInfo.version %> · <%= buildInfo.commit %></span><% } %>
</footer>
</body>
</html>
+51
View File
@@ -0,0 +1,51 @@
<%- include('top') %>
<h1>Dashboard</h1>
<div class="tiles">
<div class="tile"><span class="n"><%= metrics.active %></span><span class="l">active sessions</span></div>
<div class="tile"><span class="n"><%= metrics.total %></span><span class="l">total connections</span></div>
<div class="tile"><span class="n"><%= metrics.fail %></span><span class="l">failed</span></div>
</div>
<div class="cols">
<section>
<h2>Active sessions</h2>
<% if (!active.length) { %><p class="muted">None right now.</p><% } else { %>
<table>
<thead><tr><th>User</th><th>Target</th><th>Since</th></tr></thead>
<tbody>
<% active.forEach(s => { %>
<tr><td><%= s.uid %></td><td><%= s.slug || s.target %></td><td><%= new Date(s.startedAt).toLocaleTimeString() %></td></tr>
<% }) %>
</tbody>
</table>
<% } %>
</section>
<section>
<h2>Top hosts</h2>
<% if (!metrics.topHosts.length) { %><p class="muted">No data.</p><% } else { %>
<table><tbody>
<% metrics.topHosts.forEach(h => { %><tr><td><%= h.name %></td><td class="r"><%= h.count %></td></tr><% }) %>
</tbody></table>
<% } %>
</section>
</div>
<section>
<h2>Recent connections <a class="more" href="/audit">view all →</a></h2>
<table>
<thead><tr><th>Time</th><th>User</th><th>Target</th><th>Method</th><th>Result</th></tr></thead>
<tbody>
<% recent.forEach(e => { %>
<tr>
<td><%= new Date(e.ts).toLocaleString() %></td>
<td><%= e.uid %></td>
<td><%= e.targetSlug || e.targetAddr || '—' %></td>
<td><%= e.authMethod %> / <%= e.mode %></td>
<td><%= e.success ? '✓' : '✗ ' + e.failReason %></td>
</tr>
<% }) %>
</tbody>
</table>
</section>
<%- include('bottom') %>
+19
View File
@@ -0,0 +1,19 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><%= name %> — Jump Host login</title>
<link rel="stylesheet" href="/public/css/app.css">
</head>
<body class="center">
<form method="post" action="/login" class="card login">
<h1><%= name %> <small>jump host</small></h1>
<% if (error) { %><p class="err"><%= error %></p><% } %>
<label>Username <input name="uid" autofocus autocomplete="username"></label>
<label>Password <input name="password" type="password" autocomplete="current-password"></label>
<button type="submit">Sign in</button>
<p class="hint">Admin group required. Uses your directory (LDAP) credentials.</p>
</form>
</body>
</html>
+13
View File
@@ -0,0 +1,13 @@
<%- include('top') %>
<h1>Active sessions</h1>
<% if (!active.length) { %><p class="muted">No active sessions.</p><% } else { %>
<table>
<thead><tr><th>User</th><th>Target host</th><th>Address</th><th>Started</th></tr></thead>
<tbody>
<% active.forEach(s => { %>
<tr><td><%= s.uid %></td><td><%= s.slug || '—' %></td><td><%= s.target %></td><td><%= new Date(s.startedAt).toLocaleString() %></td></tr>
<% }) %>
</tbody>
</table>
<% } %>
<%- include('bottom') %>
+21
View File
@@ -0,0 +1,21 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><%= name %> — Jump Host</title>
<link rel="stylesheet" href="/public/css/app.css">
</head>
<body>
<nav class="nav">
<span class="brand"><%= name %> <small>jump host</small></span>
<% if (typeof user !== 'undefined' && user) { %>
<span class="spacer"></span>
<a href="/">Dashboard</a>
<a href="/sessions">Sessions</a>
<a href="/audit">Audit</a>
<span class="who"><%= user.uid %></span>
<form method="post" action="/logout" class="inline"><button class="link">logout</button></form>
<% } %>
</nav>
<main class="wrap">