Files
jump-host/nodejs/utils/key_inject.js
T
wmantly 36e9d5b0b3 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>
2026-07-23 15:48:49 -04:00

47 lines
1.8 KiB
JavaScript

'use strict';
// Upstream auth: the jump host connects to downstream hosts as the real user
// with the jump host's OWN private key. For downstream sshd to accept it, the
// jump host's public key must be one of the user's sshPublicKey values in
// LDAP (downstream hosts serve keys from LDAP via ldap-client's
// AuthorizedKeysCommand / SSSD).
//
// So: before the first upstream connect for a user, append the jump host's
// public line (comment-marked, e.g. "... jump-host@local") to their
// sshPublicKey attribute. Idempotent: exact-value duplicates are a no-op
// (TypeOrValueExists handled in models/user_ldap.addSshKey). The redis flag
// jump_host_injected_<uid> skips the LDAP round-trip on later connects; a
// failed upstream auth clears it so a manually-removed key gets re-injected
// once (see services/bridge.js).
//
// The bind DN therefore needs WRITE access to sshPublicKey on ou=people —
// documented in the README (OpenLDAP ACL) and granted by theta-env's
// bootstrap for the bundled deployment.
const conf = require('@simpleworkjs/conf');
const userLdap = require('../models/user_ldap');
const { getRedis } = require('../models');
function flagKey(uid) {
return `${conf.redis.prefix}injected_${uid}`;
}
async function ensureKeyInjected(user, publicLine, { ldap = userLdap } = {}) {
const redis = await getRedis();
if (await redis.get(flagKey(user.uid))) return false;
const already = (user.sshPublicKeys || []).includes(publicLine);
if (!already) {
await ldap.addSshKey(user.dn, publicLine);
}
await redis.set(flagKey(user.uid), '1');
return !already; // true if we actually wrote (caller may pause for SSSD cache)
}
async function clearInjectedFlag(uid) {
const redis = await getRedis();
await redis.del(flagKey(uid));
}
module.exports = { ensureKeyInjected, clearInjectedFlag };