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>
This commit is contained in:
+109
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Install / update the Theta42 jump host on a fresh or existing host.
|
||||
#
|
||||
# Idempotent: run to install, re-run to update. Installs system dependencies
|
||||
# (Node, Redis), force-syncs the repo at $REPO_DIR to its remote branch, and
|
||||
# symlinks the systemd unit straight from the repo so an update is just
|
||||
# "sync repo + restart".
|
||||
#
|
||||
# Secrets live at $SECRETS_FILE (/etc/jump-host/secrets.js by default), outside
|
||||
# the checkout so they survive the hard reset. First run seeds it from
|
||||
# secrets.js.example (placeholders you must fill in); later runs never touch it.
|
||||
#
|
||||
# Usage: sudo ./install.sh (override with REPO_URL=, REPO_DIR=, BRANCH=,
|
||||
# SECRETS_FILE=)
|
||||
set -euo pipefail
|
||||
export GIT_TERMINAL_PROMPT=0
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
REPO_URL="${REPO_URL:-https://github.com/theta42/jump-host.git}"
|
||||
REPO_DIR="${REPO_DIR:-/opt/theta42/jump-host}"
|
||||
BRANCH="${BRANCH:-master}"
|
||||
NODE_MAJOR=22
|
||||
SECRETS_FILE="${SECRETS_FILE:-/etc/jump-host/secrets.js}"
|
||||
DATA_DIR="${DATA_DIR:-/var/lib/jump-host}"
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "This script must be run as root (try: sudo $0)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
link(){ ln -sfn "$1" "$2"; echo "linked $2 -> $1"; }
|
||||
|
||||
pkg_version(){
|
||||
sed -n 's/^[[:space:]]*"version":[[:space:]]*"\([^"]*\)".*/\1/p' "$1" | head -1
|
||||
}
|
||||
|
||||
CURRENT_VERSION=""
|
||||
if [ -f "$REPO_DIR/nodejs/package.json" ]; then
|
||||
CURRENT_VERSION="$(pkg_version "$REPO_DIR/nodejs/package.json")"
|
||||
fi
|
||||
|
||||
echo "==> Base packages"
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq ca-certificates curl git gnupg redis-server >/dev/null
|
||||
|
||||
echo "==> Node.js ${NODE_MAJOR}.x"
|
||||
if ! command -v node >/dev/null 2>&1 || [ "$(node -v | sed 's/v\([0-9]*\).*/\1/')" -lt "$NODE_MAJOR" ]; then
|
||||
mkdir -p /etc/apt/keyrings
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
|
||||
| gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODE_MAJOR}.x nodistro main" \
|
||||
> /etc/apt/sources.list.d/nodesource.list
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq nodejs >/dev/null
|
||||
fi
|
||||
echo " node $(node -v)"
|
||||
|
||||
echo "==> Redis (enable + start)"
|
||||
systemctl enable --now redis-server >/dev/null 2>&1 || systemctl enable --now redis >/dev/null 2>&1 || true
|
||||
|
||||
echo "==> Repo at $REPO_DIR"
|
||||
if [ -d "$REPO_DIR/.git" ]; then
|
||||
git -C "$REPO_DIR" fetch --prune origin
|
||||
git -C "$REPO_DIR" checkout -B "$BRANCH" "origin/$BRANCH"
|
||||
git -C "$REPO_DIR" reset --hard "origin/$BRANCH"
|
||||
git -C "$REPO_DIR" clean -fd
|
||||
else
|
||||
mkdir -p "$(dirname "$REPO_DIR")"
|
||||
git clone --branch "$BRANCH" "$REPO_URL" "$REPO_DIR"
|
||||
fi
|
||||
NEW_VERSION="$(pkg_version "$REPO_DIR/nodejs/package.json")"
|
||||
|
||||
echo "==> Data dir $DATA_DIR (host keys + state)"
|
||||
mkdir -p "$DATA_DIR/keys"
|
||||
chmod 700 "$DATA_DIR" "$DATA_DIR/keys"
|
||||
|
||||
echo "==> Secrets at $SECRETS_FILE"
|
||||
if [ ! -f "$SECRETS_FILE" ]; then
|
||||
mkdir -p "$(dirname "$SECRETS_FILE")"
|
||||
cp "$REPO_DIR/secrets.js.example" "$SECRETS_FILE"
|
||||
chmod 600 "$SECRETS_FILE"
|
||||
echo " seeded from secrets.js.example — EDIT IT before the service will work:"
|
||||
echo " $SECRETS_FILE"
|
||||
else
|
||||
echo " exists — left untouched"
|
||||
fi
|
||||
|
||||
echo "==> systemd unit"
|
||||
link "$REPO_DIR/ops/jump-host.service" /etc/systemd/system/jump-host.service
|
||||
|
||||
echo "==> npm install (production deps)"
|
||||
( cd "$REPO_DIR/nodejs" && (npm ci --omit=dev 2>/dev/null || npm install --omit=dev) )
|
||||
|
||||
echo "==> Start service"
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now jump-host.service
|
||||
systemctl restart jump-host.service
|
||||
|
||||
echo
|
||||
if [ -z "$CURRENT_VERSION" ]; then
|
||||
echo "Installed jump-host v${NEW_VERSION}."
|
||||
elif [ "$CURRENT_VERSION" = "$NEW_VERSION" ]; then
|
||||
echo "Already up to date (v${NEW_VERSION})."
|
||||
else
|
||||
echo "Updated jump-host v${CURRENT_VERSION} -> v${NEW_VERSION}."
|
||||
fi
|
||||
echo "Re-run this script any time to update: sudo $0"
|
||||
echo "Logs: journalctl -u jump-host -f"
|
||||
@@ -0,0 +1,21 @@
|
||||
[Unit]
|
||||
Description=Theta42 SSH jump host
|
||||
After=network.target redis-server.service
|
||||
Wants=redis-server.service
|
||||
StartLimitIntervalSec=0
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
Restart=always
|
||||
RestartSec=1
|
||||
User=root
|
||||
WorkingDirectory=/opt/theta42/jump-host/nodejs
|
||||
Environment="NODE_ENV=production"
|
||||
Environment="CONF_SECRETS=/etc/jump-host/secrets.js"
|
||||
ExecStart=/usr/bin/env node /opt/theta42/jump-host/nodejs/bin/www
|
||||
# The default listen port is 2222 (no privilege needed). To run on 22, set
|
||||
# ssh.listenPort in secrets.js AND uncomment the next line:
|
||||
# AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Reference in New Issue
Block a user