Skip to content

Commit dc22703

Browse files
authored
Merge pull request #488 from billchurch/feature/host-key-verification
feat: SSH host key verification (TOFU)
2 parents c33a7e8 + 226f181 commit dc22703

File tree

59 files changed

+4523
-239
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+4523
-239
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,6 @@ config.json.backup
6161

6262
release-artifacts/
6363

64+
data/
65+
66+
DOCS/plans/*

DOCS/configuration/CONFIG-JSON.md

Lines changed: 170 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,19 @@ These settings are now managed client-side.
185185
"envAllowlist": ["ONLY_THIS", "AND_THAT"],
186186
"maxExecOutputBytes": 10485760,
187187
"outputRateLimitBytesPerSec": 0,
188-
"socketHighWaterMark": 16384
188+
"socketHighWaterMark": 16384,
189+
"hostKeyVerification": {
190+
"enabled": false,
191+
"mode": "hybrid",
192+
"unknownKeyAction": "prompt",
193+
"serverStore": {
194+
"enabled": true,
195+
"dbPath": "/data/hostkeys.db"
196+
},
197+
"clientStore": {
198+
"enabled": true
199+
}
200+
}
189201
},
190202
"options": {
191203
"challengeButton": true,
@@ -419,3 +431,160 @@ These options can also be configured via environment variables:
419431
- `WEBSSH2_SSH_SFTP_TIMEOUT`
420432

421433
See [ENVIRONMENT-VARIABLES.md](./ENVIRONMENT-VARIABLES.md) for details on environment variable format and examples.
434+
435+
### Host Key Verification
436+
437+
SSH host key verification provides TOFU (Trust On First Use) protection against man-in-the-middle attacks. It supports three modes of operation: server-only (SQLite store), client-only (browser localStorage), and hybrid (server-first with client fallback).
438+
439+
#### Configuration Options
440+
441+
- `ssh.hostKeyVerification.enabled` (boolean, default: `false`): Enable or disable host key verification. When disabled (the default), all host keys are accepted without verification.
442+
443+
- `ssh.hostKeyVerification.mode` (`'server'` | `'client'` | `'hybrid'`, default: `'hybrid'`): Operational mode. `server` uses only the SQLite store, `client` uses only the browser localStorage store, `hybrid` checks the server store first and falls back to the client store for unknown keys. The mode sets sensible defaults for which stores are enabled, but explicit store flags override mode defaults.
444+
445+
- `ssh.hostKeyVerification.unknownKeyAction` (`'prompt'` | `'alert'` | `'reject'`, default: `'prompt'`): Action when an unknown key is encountered (no match in any enabled store). `prompt` asks the user to accept or reject, `alert` allows the connection with a warning, `reject` blocks the connection.
446+
447+
- `ssh.hostKeyVerification.serverStore.enabled` (boolean): Whether the server-side SQLite store is active. Defaults are derived from `mode` but can be overridden explicitly.
448+
449+
- `ssh.hostKeyVerification.serverStore.dbPath` (string, default: `'/data/hostkeys.db'`): Path to the SQLite database file. The application opens it read-only. Use `npm run hostkeys` to manage keys.
450+
451+
- `ssh.hostKeyVerification.clientStore.enabled` (boolean): Whether the client-side browser localStorage store is active. Defaults are derived from `mode` but can be overridden explicitly.
452+
453+
#### Default Host Key Verification Configuration
454+
455+
```json
456+
{
457+
"ssh": {
458+
"hostKeyVerification": {
459+
"enabled": false,
460+
"mode": "hybrid",
461+
"unknownKeyAction": "prompt",
462+
"serverStore": {
463+
"enabled": true,
464+
"dbPath": "/data/hostkeys.db"
465+
},
466+
"clientStore": {
467+
"enabled": true
468+
}
469+
}
470+
}
471+
}
472+
```
473+
474+
> **Note:** Host key verification is disabled by default. Set `enabled` to `true` to activate it.
475+
476+
#### Use Cases
477+
478+
**Enable with hybrid mode (recommended):**
479+
```json
480+
{
481+
"ssh": {
482+
"hostKeyVerification": {
483+
"enabled": true,
484+
"mode": "hybrid"
485+
}
486+
}
487+
}
488+
```
489+
Server store is checked first. If the key is unknown on the server, the client's browser store is consulted. Unknown keys prompt the user.
490+
491+
**Server-only mode (centrally managed keys):**
492+
```json
493+
{
494+
"ssh": {
495+
"hostKeyVerification": {
496+
"enabled": true,
497+
"mode": "server",
498+
"unknownKeyAction": "reject"
499+
}
500+
}
501+
}
502+
```
503+
Only the server SQLite store is used. Unknown keys are rejected — administrators must pre-seed keys via `npm run hostkeys`.
504+
505+
**Client-only mode (no server database):**
506+
```json
507+
{
508+
"ssh": {
509+
"hostKeyVerification": {
510+
"enabled": true,
511+
"mode": "client"
512+
}
513+
}
514+
}
515+
```
516+
Only the client browser store is used. Users manage their own trusted keys via the settings UI.
517+
518+
**Alert-only (log but don't block):**
519+
```json
520+
{
521+
"ssh": {
522+
"hostKeyVerification": {
523+
"enabled": true,
524+
"mode": "server",
525+
"unknownKeyAction": "alert"
526+
}
527+
}
528+
}
529+
```
530+
Unknown keys show a warning indicator but connections proceed. Useful for monitoring before enforcing.
531+
532+
**Override mode defaults with explicit flags:**
533+
```json
534+
{
535+
"ssh": {
536+
"hostKeyVerification": {
537+
"enabled": true,
538+
"mode": "server",
539+
"serverStore": { "enabled": true, "dbPath": "/data/hostkeys.db" },
540+
"clientStore": { "enabled": true }
541+
}
542+
}
543+
}
544+
```
545+
Mode is `server` but `clientStore.enabled` is explicitly set to `true`, making it behave like hybrid. Explicit flags always take precedence over mode defaults.
546+
547+
#### Seeding the Server Store
548+
549+
Use the built-in CLI tool to manage the SQLite database:
550+
551+
```bash
552+
# Probe a host and add its key
553+
npm run hostkeys -- --host server1.example.com
554+
555+
# Probe a host on a non-standard port
556+
npm run hostkeys -- --host server1.example.com:2222
557+
558+
# Import from OpenSSH known_hosts file
559+
npm run hostkeys -- --known-hosts ~/.ssh/known_hosts
560+
561+
# List all stored keys
562+
npm run hostkeys -- --list
563+
564+
# Remove keys for a host
565+
npm run hostkeys -- --remove server1.example.com
566+
567+
# Use a custom database path
568+
npm run hostkeys -- --db /custom/path/hostkeys.db --host server1.example.com
569+
```
570+
571+
#### Environment Variables
572+
573+
These options can also be configured via environment variables:
574+
- `WEBSSH2_SSH_HOSTKEY_ENABLED`
575+
- `WEBSSH2_SSH_HOSTKEY_MODE`
576+
- `WEBSSH2_SSH_HOSTKEY_UNKNOWN_ACTION`
577+
- `WEBSSH2_SSH_HOSTKEY_DB_PATH`
578+
- `WEBSSH2_SSH_HOSTKEY_SERVER_ENABLED`
579+
- `WEBSSH2_SSH_HOSTKEY_CLIENT_ENABLED`
580+
581+
See [ENVIRONMENT-VARIABLES.md](./ENVIRONMENT-VARIABLES.md) for details.
582+
- `WEBSSH2_SSH_SFTP_MAX_FILE_SIZE`
583+
- `WEBSSH2_SSH_SFTP_TRANSFER_RATE_LIMIT_BYTES_PER_SEC`
584+
- `WEBSSH2_SSH_SFTP_CHUNK_SIZE`
585+
- `WEBSSH2_SSH_SFTP_MAX_CONCURRENT_TRANSFERS`
586+
- `WEBSSH2_SSH_SFTP_ALLOWED_PATHS`
587+
- `WEBSSH2_SSH_SFTP_BLOCKED_EXTENSIONS`
588+
- `WEBSSH2_SSH_SFTP_TIMEOUT`
589+
590+
See [ENVIRONMENT-VARIABLES.md](./ENVIRONMENT-VARIABLES.md) for details on environment variable format and examples.

DOCS/configuration/CONSTANTS.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,30 @@ Used in:
5656

5757
- `app/security-headers.ts` (`SECURITY_HEADERS`, `createCSPMiddleware`)
5858

59+
## SOCKET_EVENTS (Host Key Verification)
60+
61+
Location: `app/constants/socket-events.ts`
62+
63+
The following socket events were added for host key verification:
64+
65+
| Constant | Event Name | Direction | Description |
66+
|----------|-----------|-----------|-------------|
67+
| `HOSTKEY_VERIFY` | `hostkey:verify` | Server → Client | Request client to verify an unknown host key |
68+
| `HOSTKEY_VERIFY_RESPONSE` | `hostkey:verify-response` | Client → Server | Client's accept/reject/trusted decision |
69+
| `HOSTKEY_VERIFIED` | `hostkey:verified` | Server → Client | Key verified successfully, connection proceeds |
70+
| `HOSTKEY_MISMATCH` | `hostkey:mismatch` | Server → Client | Key mismatch detected, connection refused |
71+
| `HOSTKEY_ALERT` | `hostkey:alert` | Server → Client | Unknown key warning (connection proceeds) |
72+
| `HOSTKEY_REJECTED` | `hostkey:rejected` | Server → Client | Unknown key rejected by policy |
73+
74+
See [host-key-protocol.md](../host-key-protocol.md) for full payload schemas and sequence diagrams.
75+
5976
## Where These Are Used
6077

6178
- Routing and connection setup: `app/routes-v2.ts`, `app/connection/connectionHandler.ts`
6279
- Middleware and security: `app/middleware.ts`, `app/security-headers.ts`
6380
- SSH behavior and env handling: `app/services/ssh/ssh-service.ts`
6481
- Socket behavior: `app/socket-v2.ts`, `app/socket/adapters/service-socket-adapter.ts`
82+
- Host key verification: `app/services/host-key/host-key-verifier.ts`
6583

6684
## Conventions
6785

DOCS/configuration/ENVIRONMENT-VARIABLES.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,67 @@ The server applies security headers and a Content Security Policy (CSP) by defau
9393
| `WEBSSH2_SSH_OUTPUT_RATE_LIMIT_BYTES_PER_SEC` | number | `0` (unlimited) | Rate limit for shell output streams (bytes/second). `0` disables rate limiting |
9494
| `WEBSSH2_SSH_SOCKET_HIGH_WATER_MARK` | number | `16384` (16KB) | Socket.IO buffer threshold for stream backpressure control |
9595

96+
### Host Key Verification
97+
98+
| Variable | Type | Default | Description |
99+
|----------|------|---------|-------------|
100+
| `WEBSSH2_SSH_HOSTKEY_ENABLED` | boolean | `false` | Enable or disable SSH host key verification |
101+
| `WEBSSH2_SSH_HOSTKEY_MODE` | string | `hybrid` | Verification mode: `server`, `client`, or `hybrid` |
102+
| `WEBSSH2_SSH_HOSTKEY_UNKNOWN_ACTION` | string | `prompt` | Action for unknown keys: `prompt`, `alert`, or `reject` |
103+
| `WEBSSH2_SSH_HOSTKEY_DB_PATH` | string | `/data/hostkeys.db` | Path to the SQLite host key database (opened read-only by the app) |
104+
| `WEBSSH2_SSH_HOSTKEY_SERVER_ENABLED` | boolean | *(from mode)* | Override: enable/disable server-side SQLite store |
105+
| `WEBSSH2_SSH_HOSTKEY_CLIENT_ENABLED` | boolean | *(from mode)* | Override: enable/disable client-side browser store |
106+
107+
#### Host Key Verification Examples
108+
109+
**Enable hybrid mode (server-first, client fallback):**
110+
111+
```bash
112+
WEBSSH2_SSH_HOSTKEY_ENABLED=true
113+
WEBSSH2_SSH_HOSTKEY_MODE=hybrid
114+
```
115+
116+
**Server-only with strict rejection of unknown keys:**
117+
118+
```bash
119+
WEBSSH2_SSH_HOSTKEY_ENABLED=true
120+
WEBSSH2_SSH_HOSTKEY_MODE=server
121+
WEBSSH2_SSH_HOSTKEY_UNKNOWN_ACTION=reject
122+
WEBSSH2_SSH_HOSTKEY_DB_PATH=/data/hostkeys.db
123+
```
124+
125+
**Client-only (no server database needed):**
126+
127+
```bash
128+
WEBSSH2_SSH_HOSTKEY_ENABLED=true
129+
WEBSSH2_SSH_HOSTKEY_MODE=client
130+
```
131+
132+
**Docker with host key database volume:**
133+
134+
```bash
135+
docker run -d \
136+
-p 2222:2222 \
137+
-v /path/to/hostkeys.db:/data/hostkeys.db:ro \
138+
-e WEBSSH2_SSH_HOSTKEY_ENABLED=true \
139+
-e WEBSSH2_SSH_HOSTKEY_MODE=server \
140+
webssh2:latest
141+
```
142+
143+
#### Mode Behavior
144+
145+
The `mode` sets sensible defaults for which stores are enabled:
146+
147+
| Mode | Server Store | Client Store |
148+
|------|-------------|-------------|
149+
| `server` | enabled | disabled |
150+
| `client` | disabled | enabled |
151+
| `hybrid` | enabled | enabled |
152+
153+
Explicit `WEBSSH2_SSH_HOSTKEY_SERVER_ENABLED` and `WEBSSH2_SSH_HOSTKEY_CLIENT_ENABLED` override mode defaults.
154+
155+
See [CONFIG-JSON.md](./CONFIG-JSON.md) for `config.json` examples and the seeding script usage.
156+
96157
### SFTP Configuration
97158

98159
| Variable | Type | Default | Description |

0 commit comments

Comments
 (0)