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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
+9
-1
@@ -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
|
||||
|
||||
|
||||
+46
-1
@@ -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/<NODE_ENV>.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
|
||||
|
||||
+1
-1
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -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_<id>_<secret>) of any user that can read /api/discovery/*.
|
||||
// Unused when standalone.enabled is true.
|
||||
sso: {
|
||||
url: 'https://sso.example.com',
|
||||
apiToken: 'sso_CHANGE_ME',
|
||||
|
||||
Reference in New Issue
Block a user