A from-scratch SSH-2 server in TypeScript/Node. It implements the SSH
transport, authentication and connection layers by hand — version exchange,
KEXINIT negotiation, curve25519 key exchange, an ed25519 host key, the
AES-GCM binary packet protocol, publickey & password authentication, and
session channels with an interactive shell and exec. It runs on the OpenSSH
client today (ssh, scp-style exec), with zero third-party dependencies.
The crypto principle: the SSH protocol (binary packets, key-exchange orchestration, channel multiplexing, auth) is written from scratch — but the cryptographic primitives (X25519, Ed25519, AES-GCM, SHA-256, scrypt) come from Node's vetted built-in
crypto. Never roll your own AES.
$ ssh -i key -p 2222 admin@127.0.0.1 whoami
debug1: kex: algorithm: curve25519-sha256
debug1: kex: host key algorithm: ssh-ed25519
debug1: kex: server->client cipher: aes128-gcm@openssh.com
debug1: SSH2_MSG_NEWKEYS received
Authenticated to 127.0.0.1 using "publickey".
admin (role: admin)
Interactive sessions (ssh admin@host) get a jailed sandbox shell; ssh host <cmd>
runs a single command and returns its output and exit status.
| Layer | What's implemented |
|---|---|
| Transport (RFC 4253) | Version exchange, KEXINIT negotiation, curve25519-sha256 KEX, ed25519 host key + signature, key derivation, NEWKEYS, AES-256/128-GCM packet protocol |
| Authentication (RFC 4252) | publickey (ed25519 signature verification against authorized keys) and password (scrypt), banner, lockout |
| Connection (RFC 4254) | session channel, pty-req, shell, exec, env, data, exit-status, clean close |
| Shell | sandboxed command console (help, whoami, sysinfo, ls, cd, cat, …) confined by a path jail |
| Security | modern algorithms only (no sha1/cbc/3des), per-user authorized keys, path-traversal jail, no real shell by default |
| Layer | Technology |
|---|---|
| Language | TypeScript — run directly on Node (native type stripping), no build step |
| Runtime | Node.js 22.6+ (net, crypto) |
| Crypto | Node crypto: X25519, Ed25519, AES-256/128-GCM, SHA-256, scrypt |
| Storage | virtual users + authorized keys as JSON |
| Protocol | SSH-2 (RFC 4253 / 4252 / 4254) hand-written |
Zero dependencies — no
npm install. Everything is Node's standard library.
Requires Node.js 22.6+ (runs .ts files directly; Node 24 recommended).
git clone https://github.com/vugarfamiloglu/bastion-ssh
cd bastion-ssh
node src/main.ts seed # create the admin user + host key
node src/main.ts serve # listen on :2222Authorize your public key, then connect:
node src/main.ts addkey admin "$(cat ~/.ssh/id_ed25519.pub)"
ssh -p 2222 admin@127.0.0.1 # interactive sandbox shell
ssh -p 2222 admin@127.0.0.1 whoami # run a single commandPassword login also works:
ssh -p 2222 -o PreferredAuthentications=password admin@127.0.0.1 # Bastion2026!node src/main.ts serve # run the SSH server
node src/main.ts seed # create admin + host key
node src/main.ts passwd <user> <password> # set a password
node src/main.ts addkey <user> "<pubkey>" # authorize a public key
node src/main.ts version
| Variable | Default | Meaning |
|---|---|---|
BASTION_PORT |
2222 |
listen port (standard SSH is 22) |
BASTION_BIND |
0.0.0.0 |
bind address |
BASTION_DATA_DIR |
data |
host key, users.json, home trees |
BASTION_BANNER |
… | pre-auth banner |
BASTION_MAX_ATTEMPTS |
6 |
auth attempts before disconnect |
- Primitives from the library, protocol from scratch — see the principle above. Only modern algorithms are offered (curve25519, ed25519, AES-GCM); legacy sha1/cbc/3des/arcfour are not supported.
- Host key — an ed25519 key is generated on first boot and stored 0600; its fingerprint is printed at startup.
- Path-traversal jail — the sandbox shell's file commands are confined to
the user's home (
..cannot escape). Covered by unit tests. - No real shell by default — the session surface is a sandboxed command console, not a system shell. A real PTY shell is a future opt-in.
src/
main.ts CLI entrypoint (serve / seed / passwd / addkey)
config.ts BASTION_* configuration
log.ts structured logging
wire.ts SSH wire encoding (byte/uint32/string/mpint/name-list)
constants.ts message numbers + offered algorithms
hostkey.ts ed25519 host key (generate / load / sign)
kex.ts curve25519 ECDH, exchange hash, key derivation
io.ts pull-based byte reader over the socket
packet.ts binary packet protocol (none + AES-GCM modes)
transport.ts version exchange, KEXINIT, key exchange, NEWKEYS
store.ts virtual users (scrypt) + authorized keys
auth.ts userauth: publickey (sig verify) + password
vfs.ts path-traversal jail
connection.ts channels, pty-req, shell/exec
session.ts per-connection orchestration
tests/ wire / kex / vfs unit tests
- P1 — transport ✅ KEX, host key, AES-GCM packet protocol, NEWKEYS.
- P2 — authentication ✅ publickey (ed25519) + password.
- P3 — connection ✅ session channel, shell + exec, exit status.
- P4 — next ⏳ SFTP subsystem, chacha20-poly1305 cipher, port forwarding, rekeying, RSA keys, a web admin panel.
npm testUnit tests cover the SSH wire encoding (including mpint edge cases), the curve25519 ECDH agreement / exchange hash / key derivation, and the path-traversal jail. The full handshake → auth → shell flow is verified against the real OpenSSH client.
Apache License 2.0 — see LICENSE.