Files
jump-host/nodejs/utils/host_keys.js
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

57 lines
2.0 KiB
JavaScript

'use strict';
// Jump host SSH identity: one keypair used both as the server host key and
// as the client key for upstream connections (the public half is what gets
// injected into users' sshPublicKey — see utils/key_inject.js).
//
// Generated on first boot into conf.ssh.hostKeyPath:
// ed25519 (id_ed25519 / id_ed25519.pub) — primary
// rsa-3072 (id_rsa / id_rsa.pub) — compatibility host key
//
// Node's crypto generates the keys; ssh2's parseKey consumes the PEMs and
// renders the OpenSSH-format public lines.
const fs = require('fs');
const path = require('path');
const { utils: { parseKey, generateKeyPairSync } } = require('ssh2');
const conf = require('@simpleworkjs/conf');
// ssh2's parseKey wants OpenSSH-format private keys (Node's crypto PKCS8 export
// isn't accepted for ed25519), so use ssh2's own generator.
function generatePair(type) {
const { private: priv } = generateKeyPairSync(type === 'ed25519' ? 'ed25519' : 'rsa',
type === 'ed25519' ? undefined : { bits: 3072 });
return priv;
}
function pubLine(privPem, comment) {
const parsed = parseKey(privPem);
if (parsed instanceof Error) throw parsed;
const key = Array.isArray(parsed) ? parsed[0] : parsed;
return `${key.type} ${key.getPublicSSH().toString('base64')} ${comment}`;
}
function ensureKeys(dir) {
dir = dir || (conf.ssh && conf.ssh.hostKeyPath);
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
const out = {};
for (const [type, name] of [['ed25519', 'id_ed25519'], ['rsa', 'id_rsa']]) {
const priv = path.join(dir, name);
if (!fs.existsSync(priv)) {
const pem = generatePair(type);
fs.writeFileSync(priv, pem, { mode: 0o600 });
fs.writeFileSync(`${priv}.pub`, pubLine(pem, conf.ssh.keyComment) + '\n', { mode: 0o644 });
}
out[type] = fs.readFileSync(priv, 'utf8');
}
return {
hostKeys: [out.ed25519, out.rsa],
clientKey: out.ed25519,
publicLine: pubLine(out.ed25519, conf.ssh.keyComment),
};
}
module.exports = { ensureKeys, pubLine, generatePair };