Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| db3333e26d | |||
| 1092031a9f | |||
| f386a5f9c3 |
@@ -4,6 +4,34 @@ All notable changes to this project are documented here. Format loosely
|
|||||||
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions
|
||||||
correspond to git tags (`vX.Y.Z`) and `nodejs/package.json`'s `version`.
|
correspond to git tags (`vX.Y.Z`) and `nodejs/package.json`'s `version`.
|
||||||
|
|
||||||
|
## [1.14.1] - 2026-08-01
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Bumped `@simpleworkjs/bao-conf` to 1.0.1** so standalone/no-OpenBao boots
|
||||||
|
don't crash. bao-conf 1.0.0's `init()` threw when `VAULT_TOKEN` was unset,
|
||||||
|
which — combined with `bin/www`'s `.catch(() => process.exit(1))` — made the
|
||||||
|
jump host exit at boot in any deployment without an OpenBao sidecar
|
||||||
|
(standalone Docker, bare metal). 1.0.1 makes `init()` fail-soft on a missing
|
||||||
|
token (warn + continue from `CONF_SECRETS`), matching the documented
|
||||||
|
contract. The theta-env stack is unaffected (it always sets a scoped
|
||||||
|
`VAULT_TOKEN`).
|
||||||
|
|
||||||
|
## [1.14.0] - 2026-08-01
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **Secrets now load from OpenBao at boot** via
|
||||||
|
[@simpleworkjs/bao-conf](https://simpleworkjs.github.io/bao-conf/), which
|
||||||
|
deep-merges `secret/jump-host/conf` over the file-loaded config. The jump
|
||||||
|
host authenticates to OpenBao with a scoped `VAULT_TOKEN` (policy
|
||||||
|
`jump-host` — read-only on its own path), never the root token. Because the
|
||||||
|
OIDC `clientSecret` is captured at require time inside `createOidcClient`
|
||||||
|
(during `require('../models')`), `bin/www` now runs `bao-conf.init()`
|
||||||
|
**before** `require('../models')`. Fail-soft: if OpenBao is unreachable,
|
||||||
|
boot continues from `CONF_SECRETS`. The `config/jump-secrets.js` file is now
|
||||||
|
an operator-edit seed artifact (gitignored); OpenBao is authoritative. See
|
||||||
|
theta-env's [Secrets docs](https://theta42.github.io/theta-env/secrets/).
|
||||||
|
- Bumped package version to track the release tag.
|
||||||
|
|
||||||
## [1.11.0] - 2026-07-30
|
## [1.11.0] - 2026-07-30
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -159,6 +159,22 @@ Config layers via [@simpleworkjs/conf](https://www.npmjs.com/package/@simplework
|
|||||||
`conf/base.js` < `conf/<NODE_ENV>.js` < the `CONF_SECRETS` file < `app_*` env.
|
`conf/base.js` < `conf/<NODE_ENV>.js` < the `CONF_SECRETS` file < `app_*` env.
|
||||||
See `secrets.js.example` for every key.
|
See `secrets.js.example` for every key.
|
||||||
|
|
||||||
|
## Secrets
|
||||||
|
|
||||||
|
At boot, [@simpleworkjs/bao-conf](https://simpleworkjs.github.io/bao-conf/)
|
||||||
|
deep-merges `secret/jump-host/conf` from **OpenBao** over the file-loaded
|
||||||
|
config. The jump host's OIDC `clientSecret` is captured at require time
|
||||||
|
(inside `createOidcClient` during `require('../models')`), so `bin/www` runs
|
||||||
|
`bao-conf.init()` **before** `require('../models')`. Fail-soft: if OpenBao is
|
||||||
|
unreachable, boot continues from `CONF_SECRETS`. The jump host authenticates to
|
||||||
|
OpenBao with the scoped `VAULT_TOKEN` (env, policy `jump-host` — read only
|
||||||
|
`secret/jump-host/conf`), never the root token.
|
||||||
|
|
||||||
|
The `config/jump-secrets.js` file is an operator-edit seed artifact
|
||||||
|
(gitignored); the bootstrap writes the generated API token + OAuth client
|
||||||
|
into OpenBao, which is authoritative. For the full architecture see
|
||||||
|
theta-env's **[Secrets docs](https://theta42.github.io/theta-env/secrets/)**.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
+35
-24
@@ -9,32 +9,43 @@ const http = require('http');
|
|||||||
const conf = require('@simpleworkjs/conf');
|
const conf = require('@simpleworkjs/conf');
|
||||||
const { Server } = require('socket.io');
|
const { Server } = require('socket.io');
|
||||||
|
|
||||||
require('../models');
|
// @simpleworkjs/conf loads ./config/jump-secrets.js synchronously, then
|
||||||
|
// @simpleworkjs/bao-conf deep-merges secret/jump-host/conf from OpenBao over
|
||||||
|
// it. The OIDC clientSecret is captured at require time inside models (via
|
||||||
|
// createOidcClient), so the fetch MUST resolve before require('../models').
|
||||||
|
// Fail-soft: if OpenBao is unreachable, init() leaves conf as the file-loaded
|
||||||
|
// fallback and boot continues from ./config/jump-secrets.js.
|
||||||
|
require('@simpleworkjs/bao-conf').init({ path: 'jump-host', conf }).then(() => {
|
||||||
|
require('../models');
|
||||||
|
|
||||||
const app = require('../app');
|
const app = require('../app');
|
||||||
const middleware = require('../middleware/auth');
|
const middleware = require('../middleware/auth');
|
||||||
const sshServer = require('../services/ssh_server');
|
const sshServer = require('../services/ssh_server');
|
||||||
|
|
||||||
const webPort = (conf.web && conf.web.port) || 3002;
|
const webPort = (conf.web && conf.web.port) || 3002;
|
||||||
const server = http.createServer(app);
|
const server = http.createServer(app);
|
||||||
|
|
||||||
// Socket.IO — the client framework (app-base.js) opens an authenticated socket.
|
// Socket.IO — the client framework (app-base.js) opens an authenticated socket.
|
||||||
// We don't push anything yet, but serving /socket.io keeps the shared front-end
|
// We don't push anything yet, but serving /socket.io keeps the shared front-end
|
||||||
// working exactly as it does in the sibling apps.
|
// working exactly as it does in the sibling apps.
|
||||||
const io = new Server(server);
|
const io = new Server(server);
|
||||||
io.use(middleware.authIO);
|
io.use(middleware.authIO);
|
||||||
app.io = io;
|
app.io = io;
|
||||||
|
|
||||||
server.listen(webPort, () => {
|
server.listen(webPort, () => {
|
||||||
console.log(`[web] jump-host UI/API on :${server.address().port}`);
|
console.log(`[web] jump-host UI/API on :${server.address().port}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
sshServer.start();
|
||||||
|
|
||||||
|
function shutdown() {
|
||||||
|
console.log('[jump-host] shutting down');
|
||||||
|
server.close();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
process.on('SIGTERM', shutdown);
|
||||||
|
process.on('SIGINT', shutdown);
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('boot failed:', err);
|
||||||
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
sshServer.start();
|
|
||||||
|
|
||||||
function shutdown() {
|
|
||||||
console.log('[jump-host] shutting down');
|
|
||||||
server.close();
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
process.on('SIGTERM', shutdown);
|
|
||||||
process.on('SIGINT', shutdown);
|
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
// values (LDAP creds, SSO API token) belong in the secrets file.
|
// values (LDAP creds, SSO API token) belong in the secrets file.
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
name: 'Jump Host',
|
name: 'SSO Manager',
|
||||||
logo: '/static/img/theta42.svg',
|
logo: '/static/img/theta42.svg',
|
||||||
|
|
||||||
// LDAP directory the users live in (same directory the SSO manages).
|
// LDAP directory the users live in (same directory the SSO manages).
|
||||||
|
|||||||
Generated
+15
-2
@@ -1,16 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "t42-jump-host",
|
"name": "t42-jump-host",
|
||||||
"version": "1.9.0",
|
"version": "1.14.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "t42-jump-host",
|
"name": "t42-jump-host",
|
||||||
"version": "1.9.0",
|
"version": "1.14.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^7.3.0",
|
"@fortawesome/fontawesome-free": "^7.3.0",
|
||||||
"@simpleworkjs/app-stack": "^1.0.0",
|
"@simpleworkjs/app-stack": "^1.0.0",
|
||||||
|
"@simpleworkjs/bao-conf": "^1.0.0",
|
||||||
"@simpleworkjs/conf": "^1.2.0",
|
"@simpleworkjs/conf": "^1.2.0",
|
||||||
"@simpleworkjs/directory-schema": "^1.0.0",
|
"@simpleworkjs/directory-schema": "^1.0.0",
|
||||||
"@simpleworkjs/frontend": "^0.2.6",
|
"@simpleworkjs/frontend": "^0.2.6",
|
||||||
@@ -156,6 +157,18 @@
|
|||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@simpleworkjs/bao-conf": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@simpleworkjs/bao-conf/-/bao-conf-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-mcay5NQ/w9ShpIAolMP/3f9TfXSLE+d5jrA4dTPOUHDjTkdsP7pe4hMmQUmwnniR59U1bGoRIVdXjvDbX3I5nw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"extend": "^3.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@simpleworkjs/conf": {
|
"node_modules/@simpleworkjs/conf": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@simpleworkjs/conf/-/conf-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@simpleworkjs/conf/-/conf-1.2.0.tgz",
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "t42-jump-host",
|
"name": "t42-jump-host",
|
||||||
"version": "1.11.0",
|
"version": "1.14.1",
|
||||||
"description": "SSH jump host for the theta42 stack — LDAP-authenticated, directory-driven host bridging with audit and metrics",
|
"description": "SSH jump host for the theta42 stack — LDAP-authenticated, directory-driven host bridging with audit and metrics",
|
||||||
"author": [
|
"author": [
|
||||||
{
|
{
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^7.3.0",
|
"@fortawesome/fontawesome-free": "^7.3.0",
|
||||||
"@simpleworkjs/app-stack": "^1.0.0",
|
"@simpleworkjs/app-stack": "^1.0.0",
|
||||||
|
"@simpleworkjs/bao-conf": "^1.0.0",
|
||||||
"@simpleworkjs/conf": "^1.2.0",
|
"@simpleworkjs/conf": "^1.2.0",
|
||||||
"@simpleworkjs/directory-schema": "^1.0.0",
|
"@simpleworkjs/directory-schema": "^1.0.0",
|
||||||
"@simpleworkjs/frontend": "^0.2.6",
|
"@simpleworkjs/frontend": "^0.2.6",
|
||||||
|
|||||||
@@ -9,10 +9,29 @@ const ESC = '\x1b';
|
|||||||
const CLEAR = `${ESC}[2J${ESC}[H`;
|
const CLEAR = `${ESC}[2J${ESC}[H`;
|
||||||
const HIDE_CUR = `${ESC}[?25l`;
|
const HIDE_CUR = `${ESC}[?25l`;
|
||||||
const SHOW_CUR = `${ESC}[?25h`;
|
const SHOW_CUR = `${ESC}[?25h`;
|
||||||
const INV = `${ESC}[7m`;
|
|
||||||
|
// Basic styles
|
||||||
const RST = `${ESC}[0m`;
|
const RST = `${ESC}[0m`;
|
||||||
const DIM = `${ESC}[2m`;
|
|
||||||
const BOLD = `${ESC}[1m`;
|
const BOLD = `${ESC}[1m`;
|
||||||
|
const DIM = `${ESC}[2m`;
|
||||||
|
|
||||||
|
// Colors (30-37: standard, 90-97: bright)
|
||||||
|
const RED = `${ESC}[31m`;
|
||||||
|
const BRIGHT_RED = `${ESC}[91m`;
|
||||||
|
const CYAN = `${ESC}[36m`;
|
||||||
|
const BRIGHT_CYAN = `${ESC}[96m`;
|
||||||
|
const GREEN = `${ESC}[32m`;
|
||||||
|
const BRIGHT_GREEN = `${ESC}[92m`;
|
||||||
|
const YELLOW = `${ESC}[33m`;
|
||||||
|
const BRIGHT_YELLOW = `${ESC}[93m`;
|
||||||
|
const MAGENTA = `${ESC}[35m`;
|
||||||
|
const BRIGHT_MAGENTA = `${ESC}[95m`;
|
||||||
|
const BLUE = `${ESC}[34m`;
|
||||||
|
const BRIGHT_BLUE = `${ESC}[94m`;
|
||||||
|
|
||||||
|
// Inverted selection with color
|
||||||
|
const INV_GREEN = `${ESC}[42m${ESC}[30m`; // Green bg, black text
|
||||||
|
const INV = `${ESC}[7m`;
|
||||||
|
|
||||||
function pickHost(channel, uid, hosts) {
|
function pickHost(channel, uid, hosts) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
@@ -35,18 +54,43 @@ function pickHost(channel, uid, hosts) {
|
|||||||
const list = visible();
|
const list = visible();
|
||||||
if (selected >= list.length) selected = Math.max(0, list.length - 1);
|
if (selected >= list.length) selected = Math.max(0, list.length - 1);
|
||||||
let out = CLEAR + HIDE_CUR;
|
let out = CLEAR + HIDE_CUR;
|
||||||
out += `${BOLD} Theta42 Jump — hosts for ${uid}${RST}\r\n`;
|
|
||||||
out += `${DIM} ↑/↓ move · Enter connect · type to filter · q quit${RST}\r\n\r\n`;
|
// Header with gradient-style color
|
||||||
|
out += `\r\n ${BOLD}${BRIGHT_CYAN}╔════════════════════════════════════════════════════════╗${RST}\r\n`;
|
||||||
|
out += ` ${BOLD}${BRIGHT_CYAN}║${RST} ${BOLD}${BRIGHT_MAGENTA}Theta42 Jump${RST} ${DIM}·${RST} ${BRIGHT_GREEN}hosts for ${uid}${RST} ${BOLD}${BRIGHT_CYAN}║${RST}\r\n`;
|
||||||
|
out += ` ${BOLD}${BRIGHT_CYAN}╚════════════════════════════════════════════════════════╝${RST}\r\n`;
|
||||||
|
out += `\r\n`;
|
||||||
|
out += ` ${DIM}↑/↓ move · Enter connect · type to filter · q quit${RST}\r\n`;
|
||||||
|
out += `\r\n`;
|
||||||
|
|
||||||
if (!list.length) {
|
if (!list.length) {
|
||||||
out += ` ${DIM}(no match for "${filter}")${RST}\r\n`;
|
out += ` ${YELLOW}⚠${RST} ${DIM}(no match for "${filter}")${RST}\r\n`;
|
||||||
} else {
|
} else {
|
||||||
list.forEach((h, i) => {
|
list.forEach((h, i) => {
|
||||||
const ip = (h.metadata && h.metadata.ip) || (h.metadata && h.metadata.address) || '';
|
const ip = (h.metadata && h.metadata.ip) || (h.metadata && h.metadata.address) || '';
|
||||||
const row = ` ${h.name} ${DIM}(${h.slug})${RST}${ip ? ` ${ip}` : ''}`;
|
const isProd = h.metadata && h.metadata.isProduction;
|
||||||
out += (i === selected ? `${INV}> ${h.name} (${h.slug})${ip ? ` ${ip}` : ''}${RST}` : row) + '\r\n';
|
const envBadge = isProd ? `${BOLD}${RED}PROD${RST} ` : `${DIM}DEV${RST} `;
|
||||||
|
|
||||||
|
if (i === selected) {
|
||||||
|
// Selected row with green inverse background
|
||||||
|
const selRow = `${INV_GREEN} ${h.name} ${DIM}(${h.slug})${RST}${ip ? ` ${CYAN}${ip}${RST}` : ''} ${envBadge} ${BOLD}${BRIGHT_GREEN}◄ SELECTED ►${RST}${INV_GREEN}${RST}`;
|
||||||
|
out += selRow + '\r\n';
|
||||||
|
} else {
|
||||||
|
// Normal row with subtle coloring
|
||||||
|
const nameColor = i % 2 === 0 ? BRIGHT_CYAN : CYAN;
|
||||||
|
out += ` ${nameColor}${h.name}${RST} ${DIM}(${h.slug})${RST}${ip ? ` ${BLUE}${ip}${RST}` : ''} ${envBadge}\r\n`;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (filter) out += `\r\n ${DIM}filter:${RST} ${filter}`;
|
|
||||||
|
if (filter) {
|
||||||
|
out += `\r\n ${DIM}filter: ${BRIGHT_YELLOW}${filter}${RST}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Footer
|
||||||
|
out += `\r\n\r\n ${DIM}────────────────────────────────────────────────────────${RST}\r\n`;
|
||||||
|
out += ` ${DIM}Press${RST} ${BOLD}1-9${RST} ${DIM}to quick-select · ${BOLD}q${RST} ${DIM}to quit${RST}\r\n`;
|
||||||
|
|
||||||
channel.write(out);
|
channel.write(out);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user