From d33e324b31da1c511196567220c926e5af4a4ac6 Mon Sep 17 00:00:00 2001 From: William Mantly Date: Sun, 26 Jul 2026 21:33:27 -0400 Subject: [PATCH] docs: document standalone mode; bump to 1.4.0 Add README/docs/secrets.js.example coverage for the new @simpleworkjs/orm-backed standalone mode (no LDAP/SSO), and promote the CHANGELOG's Unreleased entry to 1.4.0. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 11 +++++++++ README.md | 55 +++++++++++++++++++++++++++++++++++++++++--- docs/_config.yml | 2 +- docs/architecture.md | 22 ++++++++++++++++++ docs/connecting.md | 4 ++++ docs/index.md | 10 +++++++- docs/installation.md | 47 ++++++++++++++++++++++++++++++++++++- nodejs/package.json | 2 +- secrets.js.example | 18 +++++++++++++++ 9 files changed, 164 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4fe2bb..8b00630 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to this project are documented here. Format loosely 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`. +## [1.4.0] - 2026-07-26 + +### Added +- **Standalone mode** — run the jump host with no LDAP directory and no SSO Manager at all. Set `standalone.enabled: true` and user authentication and host discovery switch to `@simpleworkjs/orm`-backed stores (Sequelize; SQLite by default, any Sequelize-supported dialect via `conf.orm`) instead of the directory services. `models/user_ldap.js` and `utils/access.js` become conditional facades that pick their backend at require time — `ssh_server.js`, `bridge.js`, `key_inject.js`, `tui_picker.js`, and the web UI are unchanged either way. +- New ORM models: `StandaloneUser` (`uid`, `passwordHash`, `sshPublicKeys`, `groups`) and `StandaloneHost` (`slug`, `displayName`, `kind`, `metadata`), plus `models/user_file.js` and `utils/hosts_file.js`, which implement the same interfaces as the LDAP client and `accessibleHosts()` respectively. There's no admin UI for standalone users/hosts yet — see the README's "Standalone mode" section for the ORM-model seeding snippet. In standalone mode every stored host is reachable by every stored user; there's no group-based authorization yet. +- 47 tests pass (24 existing + 15 new unit + 3 existing integration + 5 new standalone integration). + +### Fixed +- **`services/ssh_server.js` used `|| 2222` for the listen port**, so an explicit `listenPort: 0` (ephemeral port, used by the test suite) was silently overridden back to 2222. Changed to `?? 2222`. +- **`services/ssh_server.js` awaited `audit.create()` before registering session listeners.** A client that sends `exec`/`shell` immediately after connecting could have its request dropped because nothing was listening yet. Listener registration now happens first. + ## [1.3.0] - 2026-07-26 ### Changed diff --git a/README.md b/README.md index 550fdbf..9bb2c02 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,13 @@ An SSH jump host for the [theta42](https://github.com/theta42) self-hosted stack. Users SSH into one public host and land on any downstream host they're -entitled to — authenticated against the shared LDAP directory, authorized from -the [SSO Manager](https://github.com/theta42/sso-manager-node)'s inventory -graph, audited end to end. +entitled to — audited end to end. + +Two backends, same SSH front door and audit trail: the default mode +authenticates against the shared LDAP directory and authorizes from the +[SSO Manager](https://github.com/theta42/sso-manager-node)'s inventory graph; +**standalone mode** (below) runs with no LDAP or SSO at all, storing users and +hosts in a local SQL database instead. ## Two ways to connect @@ -44,8 +48,53 @@ bridged straight in. 4. **Bridge** — shell, exec, and the SFTP subsystem are spliced to the downstream sshd. Every session is audited. +## Standalone mode + +Run without LDAP or the SSO Manager at all. Set `standalone.enabled: true` and +the jump host stores users and hosts itself, via +[@simpleworkjs/orm](https://www.npmjs.com/package/@simpleworkjs/orm) +(Sequelize under the hood — defaults to a local SQLite file, but any +Sequelize-supported dialect works via `conf.orm`): + +```js +standalone: { enabled: true }, +orm: { dialect: 'sqlite', storage: './data/standalone.sqlite', logging: false }, +``` + +Everything else — the SSH front door, key injection, bridging, the web UI and +audit trail — is unchanged; only where users/hosts live and how passwords are +checked differs. There's no admin UI for standalone users/hosts yet — add them +with the ORM models directly: + +```js +const StandaloneUser = require('./models/standalone_user'); +const StandaloneHost = require('./models/standalone_host'); +const bcrypt = require('bcrypt'); + +await StandaloneUser.create({ + uid: 'alice', + passwordHash: await bcrypt.hash('a real password', 10), + sshPublicKeys: ['ssh-ed25519 AAAA... alice@laptop'], + groups: [], +}); + +await StandaloneHost.create({ + slug: 'host_web01', + displayName: 'web01', + kind: 'host', + metadata: { ip: '10.0.0.5', sshPort: 22 }, +}); +``` + +In standalone mode every stored host is reachable by every stored user — there +is no group-based authorization (the `groups` field on `StandaloneUser` is +accepted for interface parity but not yet enforced). + ## Requirements +*(default LDAP + SSO mode — see [Standalone mode](#standalone-mode) to skip +all of this)* + - The SSO Manager (OpenLDAP directory + `/api/discovery`). - Downstream hosts joined via ldap-client (SSSD + `AuthorizedKeysCommand`). - An LDAP bind account with **write access to the `sshPublicKey` attribute** on diff --git a/docs/_config.yml b/docs/_config.yml index 1216495..eb44233 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,5 +1,5 @@ title: Jump Host -description: An SSH jump host for the theta42 stack — LDAP-authenticated, directory-driven host bridging with audit and metrics. +description: An SSH jump host for the theta42 stack — directory-driven host bridging with audit and metrics; LDAP + SSO Manager by default, or fully standalone. url: "https://theta42.github.io" baseurl: "/jump-host" logo: /assets/img/theta42.svg diff --git a/docs/architecture.md b/docs/architecture.md index c5dfbf1..8104ee1 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -112,6 +112,28 @@ Audit events and counters live in redis. Each event captures: user, auth method, mode (grammar/picker), target slug/address/port, channel type, client IP, success + failure reason, downstream host-key fingerprint, timing, and bytes in/out. +## Standalone mode + +Everything above describes the default backend. Set `standalone.enabled: true` +and two modules become conditional facades, swapping their entire +implementation at `require` time based on that flag — nothing else in the +codebase (`ssh_server.js`, `bridge.js`, `key_inject.js`, `tui_picker.js`, the +web UI) changes or even knows which mode it's running in: + +- **`models/user_ldap.js`** — LDAP client, or `models/user_file.js` (an + [@simpleworkjs/orm](https://www.npmjs.com/package/@simpleworkjs/orm)-backed + store implementing the same `getUser` / `getGroups` / `checkPassword` / + `addSshKey` interface). +- **`utils/access.js`** — LDAP groups + SSO `/api/discovery`, or + `utils/hosts_file.js` (same ORM package, same `accessibleHosts()` interface). + In standalone mode there's no group-based authorization: every stored host + is accessible to every stored user. + +The ORM is Sequelize underneath, defaulting to a local SQLite file but +accepting any Sequelize-supported dialect via `conf.orm`. See +[Installation](installation.html#standalone-mode) for config and how to add +users/hosts (there's no admin UI for standalone data yet). + ## Where it sits in the stack - **[SSO Manager](https://theta42.github.io/sso-manager-node/)** — provides the diff --git a/docs/connecting.md b/docs/connecting.md index 19eedfe..966ba87 100644 --- a/docs/connecting.md +++ b/docs/connecting.md @@ -74,6 +74,10 @@ changes. Targets that don't resolve to a host you're allowed to reach are refused (and audited). Raw IPs that aren't a known directory host are denied by default. +> On a [standalone](architecture.html#standalone-mode) jump host (no LDAP/SSO), +> every registered host is reachable by every registered user — there's no +> group-based restriction to ask an admin about. + ## Authentication The jump host authenticates **you** against the directory: diff --git a/docs/index.md b/docs/index.md index 58e36ff..b7d73bc 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,7 +1,7 @@ --- layout: default title: Home -description: An SSH jump host for the theta42 stack — one public host, LDAP login, and directory-driven access to every downstream machine you're entitled to. +description: An SSH jump host for the theta42 stack — one public host and directory-driven access to every downstream machine you're entitled to; LDAP by default, or fully standalone. --- # Jump Host @@ -30,6 +30,12 @@ Part of the theta42 self-hosted identity stack, alongside *(click any screenshot to view full size)* +Don't want to run LDAP or the SSO Manager? **Standalone mode** stores users +and hosts in a local SQL database instead (SQLite by default, any +Sequelize-supported dialect if you want something else) — same SSH front door, +key injection, and audit trail. See +[Installation](installation.html#standalone-mode) to get started. + ## Two ways to connect **Direct (WinSCP/SFTP-friendly):** @@ -88,6 +94,8 @@ This jump host answers both from your directory: audit log, per-user/per-host counters - **Full audit trail** — who, target, method, result, bytes, duration, and the downstream host-key fingerprint +- **Standalone mode** — no LDAP, no SSO Manager; users and hosts live in a + local SQL database (Sequelize, any dialect — SQLite by default) - Packaged like the rest of the stack: one-command Docker, idempotent bare-metal installer, or bundled in theta-env diff --git a/docs/installation.md b/docs/installation.md index 23de338..6a5f37b 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1,7 +1,7 @@ --- layout: default title: Installation -description: Install the jump host three ways — bundled in the theta-env stack, standalone Docker, or bare metal — plus the required LDAP write-ACL and port-22 options. +description: Install the jump host three ways — bundled in the theta-env stack, standalone Docker, or bare metal — plus standalone mode (no LDAP/SSO), the LDAP write-ACL, and port-22 options. --- # Installation @@ -10,6 +10,51 @@ Three ways to run the jump host, in increasing manual effort. All read their config through [@simpleworkjs/conf](https://www.npmjs.com/package/@simpleworkjs/conf) (`conf/base.js` < `conf/.js` < the `CONF_SECRETS` file < `app_*` env). +## Standalone mode (no LDAP/SSO) {#standalone-mode} + +Skip LDAP and the SSO Manager entirely. Not to be confused with "Standalone +Docker" below, which is still LDAP + SSO, just run outside theta-env. Set in your secrets/config: + +```js +standalone: { enabled: true }, +orm: { dialect: 'sqlite', storage: './data/standalone.sqlite', logging: false }, +``` + +`orm` is passed straight to Sequelize, so any supported dialect works — SQLite +is just the zero-dependency default. Everything downstream of auth (bridging, +key injection, the web UI, audit) is unchanged. + +There's no admin UI for standalone users/hosts yet, so add them directly with +the ORM models: + +```js +const StandaloneUser = require('./models/standalone_user'); +const StandaloneHost = require('./models/standalone_host'); +const bcrypt = require('bcrypt'); + +await StandaloneUser.create({ + uid: 'alice', + passwordHash: await bcrypt.hash('a real password', 10), + sshPublicKeys: ['ssh-ed25519 AAAA... alice@laptop'], + groups: [], +}); + +await StandaloneHost.create({ + slug: 'host_web01', + displayName: 'web01', + kind: 'host', + metadata: { ip: '10.0.0.5', sshPort: 22 }, +}); +``` + +Every host in the standalone inventory is reachable by every standalone user — +there's no group-based authorization yet (`groups` on `StandaloneUser` is +accepted for interface parity with the LDAP path, not enforced). + +The rest of this page (requirements, the LDAP write-ACL, the three install +paths) describes the default LDAP + SSO mode — skip it if you're running +standalone. + ## Requirements - The [SSO Manager](https://theta42.github.io/sso-manager-node/) (OpenLDAP diff --git a/nodejs/package.json b/nodejs/package.json index 937566a..eeb0f01 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -1,6 +1,6 @@ { "name": "t42-jump-host", - "version": "1.3.0", + "version": "1.4.0", "description": "SSH jump host for the theta42 stack — LDAP-authenticated, directory-driven host bridging with audit and metrics", "author": [ { diff --git a/secrets.js.example b/secrets.js.example index 317a069..7676caf 100644 --- a/secrets.js.example +++ b/secrets.js.example @@ -11,7 +11,24 @@ module.exports = { name: 'My Org', + // Standalone mode: run with no LDAP directory and no SSO Manager. When + // enabled, user auth and host discovery use the ORM-backed stores below + // instead of `ldap` + `sso` (both become unused). See the README's + // "Standalone mode" section for how to add users/hosts. + standalone: { + enabled: false, + }, + + // ORM config for standalone mode (Sequelize — any dialect works, not just + // sqlite). Ignored unless standalone.enabled is true. + orm: { + dialect: 'sqlite', + storage: './data/standalone.sqlite', + logging: false, + }, + // The directory the users live in (the SSO Manager's OpenLDAP). + // Unused when standalone.enabled is true. // // IMPORTANT: bindDN needs, beyond read on ou=people + ou=groups, WRITE on // the sshPublicKey attribute of user entries — the jump host injects its @@ -36,6 +53,7 @@ module.exports = { // SSO Manager directory (inventory) API. apiToken is a personal access // token (sso__) of any user that can read /api/discovery/*. + // Unused when standalone.enabled is true. sso: { url: 'https://sso.example.com', apiToken: 'sso_CHANGE_ME',