36e9d5b0b3
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>
47 lines
1.8 KiB
JavaScript
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 };
|