chore: sync project to current state of bigip-server commit:e3d6ec8

This commit is contained in:
Bill Church 2024-12-14 14:31:26 +00:00
parent a321eb2f94
commit 17b5ae7552
No known key found for this signature in database
12 changed files with 211 additions and 32 deletions

View file

@ -84,7 +84,7 @@ Renamed and expanded options:
## Detailed Changes
### 1. Authentication Options
- Added support for SSH private key authentication via `user.privateKey`
- Added support for SSH private key authentication via `user.privateKey` and passphrase encrypted private keys via `user.passphrase`
- Removed `user.overridebasic` option
- Added keyboard-interactive authentication controls
@ -128,7 +128,8 @@ These settings are now managed client-side.
"user": {
"name": null,
"password": null,
"privateKey": null
"privateKey": null,
"passphrase": null
},
"ssh": {
"host": null,
@ -138,7 +139,39 @@ These settings are now managed client-side.
"keepaliveInterval": 120000,
"keepaliveCountMax": 10,
"algorithms": {
// ... algorithm configurations ...
"cipher": [
"aes128-ctr",
"aes192-ctr",
"aes256-ctr",
"aes128-gcm",
"aes128-gcm@openssh.com",
"aes256-gcm",
"aes256-gcm@openssh.com",
"aes256-cbc"
],
"compress": [
"none",
"zlib@openssh.com",
"zlib"
],
"hmac": [
"hmac-sha2-256",
"hmac-sha2-512",
"hmac-sha1"
],
"kex": [
"ecdh-sha2-nistp256",
"ecdh-sha2-nistp384",
"ecdh-sha2-nistp521",
"diffie-hellman-group-exchange-sha256",
"diffie-hellman-group14-sha1"
],
"serverHostKey": [
"ecdsa-sha2-nistp256",
"ecdsa-sha2-nistp384",
"ecdsa-sha2-nistp521",
"ssh-rsa"
]
}
},
"options": {

View file

@ -2,6 +2,46 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
<a name="0.2.24"></a>
## [0.2.24](https://github.com/billchurch/WebSSH2/compare/v0.2.23...v0.2.24) (2024-12-04)
### Bug Fixes
* config.json.sample had `disableInteractiveAuth` set to `true`, changed to `false` ([0c5de9f](https://github.com/billchurch/WebSSH2/commit/0c5de9f))
<a name="0.2.23"></a>
## [0.2.23](https://github.com/billchurch/WebSSH2/compare/v0.2.20...v0.2.23) (2024-12-04)
### Bug Fixes
* fixes document: config file moved from /usr/src to /usr/src/app [#372](https://github.com/billchurch/WebSSH2/issues/372) ([bc2d018](https://github.com/billchurch/WebSSH2/commit/bc2d018))
* bug: support /ssh/host without a hostname [#373](https://github.com/billchurch/WebSSH2/issues/373) ([8c55c83](https://github.com/billchurch/WebSSH2/commit/8c55c83))
* config change `privatekey` to `privateKey` for consistency with ssh2 module ([a176167](https://github.com/billchurch/WebSSH2/commit/a176167))
* config move algorithims to ssh property ([52a989b](https://github.com/billchurch/WebSSH2/commit/52a989b))
* pass full ssh error to browser ([27d9bfb](https://github.com/billchurch/WebSSH2/commit/27d9bfb))
* username/password in config file no longer honored [#374](https://github.com/billchurch/WebSSH2/issues/374) ([4185df7](https://github.com/billchurch/WebSSH2/commit/4185df7))
### Features
* accept private key from client [#381](https://github.com/billchurch/WebSSH2/issues/381) ([829b5cd](https://github.com/billchurch/WebSSH2/commit/829b5cd))
* add `ssh.disableInteractiveAuth` feature in support of [#379](https://github.com/billchurch/WebSSH2/issues/379) ([c7dfad0](https://github.com/billchurch/WebSSH2/commit/c7dfad0))
* allow passphrase encrypted ssh keys from client [#381](https://github.com/billchurch/WebSSH2/issues/381) ([056e87b](https://github.com/billchurch/WebSSH2/commit/056e87b))
* Allow setting environment variables from the URL [#371](https://github.com/billchurch/WebSSH2/issues/371) ([6ec0490](https://github.com/billchurch/WebSSH2/commit/6ec0490))
* implement ssh private key auth [#379](https://github.com/billchurch/WebSSH2/issues/379) ([402b678](https://github.com/billchurch/WebSSH2/commit/402b678))
* passphrase encrypted private key authentication [#382](https://github.com/billchurch/WebSSH2/issues/382) ([7961451](https://github.com/billchurch/WebSSH2/commit/7961451))
* support uploading of ssh-rsa private key from client for authentication [#381](https://github.com/billchurch/WebSSH2/issues/381) ([2f4083f](https://github.com/billchurch/WebSSH2/commit/2f4083f))
* update jsmasker to v1.4.0 ([3315df1](https://github.com/billchurch/WebSSH2/commit/3315df1))
* update webssh_client to 0.2.26 ([a1b2e56](https://github.com/billchurch/WebSSH2/commit/a1b2e56))
* update webssh2_client to 0.2.27 ([b511ce5](https://github.com/billchurch/WebSSH2/commit/b511ce5))
* webssh2_client to 0.2.28 ([b4b7429](https://github.com/billchurch/WebSSH2/commit/b4b7429))
<a name="0.2.22"></a>
## [0.2.22](https://github.com/billchurch/WebSSH2/compare/v0.2.21...v0.2.22) (2024-11-30)

View file

@ -149,7 +149,7 @@ If you encounter issues:
4. Verify the ports (3000 and 2222) are available
5. Clear browser cache if changes aren't reflecting
## Building for Production
## Building for Production (client)
When ready to build for production:

View file

@ -196,7 +196,7 @@ For more information on SSH keyboard-interactive authentication, refer to [RFC 4
### SSH Private Key Authentication
WebSSH2 supports SSH private key authentication when using the `/ssh/host/` endpoint with a private key configured in the server settings.
WebSSH2 supports SSH private key authentication when using the `/ssh/host/` endpoint with a private key configured in the server settings or via the interactive method with the `/ssh/` endpoint.
#### Configuration
@ -215,13 +215,17 @@ Private key authentication can only be configured through the `config.json` file
#### Key Requirements
- Only `ssh-rsa` type keys are supported
- Passphrase encryption is supported, and if used the `passphrase` must be provided
- The private key must be in PEM format
- The key in `config.json` must be on a single line with `\n` as line separators
- Must include the appropriate header and footer:
```
-----BEGIN RSA PRIVATE KEY-----\n[... key content ...]\n-----END RSA PRIVATE KEY-----
```
or for encrypted keys:
```
-----BEGIN RSA PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-128-CBC,5930F19760F7FBBC865400940A89D954\n\n[... key content ...]\n-----END RSA PRIVATE KEY-----
```
#### Generating a Private Key
To generate a new SSH private key, you can use the following command:
@ -231,10 +235,10 @@ ssh-keygen -m PEM -t rsa -b 4096 -f ~/.ssh/id_rsa
#### Converting Your Private Key
To convert your existing SSH private key into the correct format for `config.json`, you can use this bash command:
Keys uploaded or pasted using the interactive mode through the `/ssh` endpoint can work as-is, however if using a key with `config.json` you must convert your existing SSH private key into the correct format (single line). A bash one-liner you can to accomplish this is:
```bash
echo '"'$(cat ~/.ssh/id_rsa | tr '\n' '~' | sed 's/~/\\n/g')'"'
echo ' "privateKey": "'$(cat ~/.ssh/id_rsa | tr '\n' '~' | sed 's/~/\\n/g')'"'
```
This command:

View file

@ -3,6 +3,7 @@
import express from 'express'
import config from './config.js'
import SSHConnection from './ssh.js'
import socketHandler from './socket.js'
import { createRoutes } from './routes.js'
import { applyMiddleware } from './middleware.js'
@ -52,7 +53,7 @@ function initializeServer() {
const io = configureSocketIO(server, sessionMiddleware, config)
// Set up Socket.IO listeners
socketHandler(io, config)
socketHandler(io, config, SSHConnection)
// Start the server
startServer(server, config)

View file

@ -23,6 +23,8 @@ const defaultConfig = {
user: {
name: null,
password: null,
privateKey: null,
passphrase: null,
},
ssh: {
host: null,

View file

@ -28,6 +28,7 @@ const configSchema = {
name: { type: ['string', 'null'] },
password: { type: ['string', 'null'] },
privateKey: { type: ['string', 'null'] },
passphrase: { type: ['string', 'null'] },
},
required: ['name', 'password'],
},

View file

@ -3,20 +3,27 @@
import validator from 'validator'
import { EventEmitter } from 'events'
import SSHConnection from './ssh.js'
import { createNamespacedDebug } from './logger.js'
import { SSHConnectionError, handleError } from './errors.js'
const debug = createNamespacedDebug('socket')
import { isValidCredentials, maskSensitiveData, validateSshTerm } from './utils.js'
import { MESSAGES } from './constants.js'
const debug = createNamespacedDebug('socket')
class WebSSH2Socket extends EventEmitter {
constructor(socket, config) {
/**
* Creates a new WebSSH2Socket instance
* @param {Object} socket - The Socket.IO socket instance
* @param {Object} config - The application configuration
* @param {Function} SSHConnectionClass - The SSH connection class constructor
*/
constructor(socket, config, SSHConnectionClass) {
super()
this.socket = socket
this.config = config
this.ssh = new SSHConnection(config)
this.SSHConnectionClass = SSHConnectionClass
this.ssh = null
this.sessionState = {
authenticated: false,
username: null,
@ -58,10 +65,6 @@ class WebSSH2Socket extends EventEmitter {
this.socket.emit('authentication', { action: 'request_auth' })
}
this.ssh.on('keyboard-interactive', (data) => {
this.handleKeyboardInteractive(data)
})
this.socket.on('authenticate', (creds) => {
this.handleAuthenticate(creds)
})
@ -129,6 +132,14 @@ class WebSSH2Socket extends EventEmitter {
creds.privateKey = this.config.user.privateKey
}
// Create new SSH connection instance
this.ssh = new this.SSHConnectionClass(this.config)
// Set up SSH event handlers
this.ssh.on("keyboard-interactive", data => {
this.handleKeyboardInteractive(data)
})
this.ssh
.connect(creds)
.then(() => {
@ -342,6 +353,6 @@ class WebSSH2Socket extends EventEmitter {
}
}
export default function (io, config) {
io.on('connection', (socket) => new WebSSH2Socket(socket, config))
export default function (io, config, SSHConnectionClass) {
io.on('connection', (socket) => new WebSSH2Socket(socket, config, SSHConnectionClass))
}

View file

@ -25,14 +25,30 @@ class SSHConnection extends EventEmitter {
}
/**
* Validates the format of an RSA private key
* Validates the format of an RSA private key, supporting both standard and encrypted keys
* @param {string} key - The private key string to validate
* @returns {boolean} - Whether the key appears to be valid
*/
validatePrivateKey(key) {
const keyPattern =
// Pattern for standard RSA private key
const standardKeyPattern =
/^-----BEGIN (?:RSA )?PRIVATE KEY-----\r?\n([A-Za-z0-9+/=\r\n]+)\r?\n-----END (?:RSA )?PRIVATE KEY-----\r?\n?$/
return keyPattern.test(key)
// Pattern for encrypted RSA private key
const encryptedKeyPattern =
/^-----BEGIN RSA PRIVATE KEY-----\r?\n(?:Proc-Type: 4,ENCRYPTED\r?\nDEK-Info: ([^\r\n]+)\r?\n\r?\n)([A-Za-z0-9+/=\r\n]+)\r?\n-----END RSA PRIVATE KEY-----\r?\n?$/
// Test for either standard or encrypted key format
return standardKeyPattern.test(key) || encryptedKeyPattern.test(key)
}
/**
* Checks if a private key is encrypted
* @param {string} key - The private key to check
* @returns {boolean} - Whether the key is encrypted
*/
isEncryptedKey(key) {
return key.includes('Proc-Type: 4,ENCRYPTED')
}
/**
@ -126,12 +142,68 @@ class SSHConnection extends EventEmitter {
this.handleKeyboardInteractive(name, instructions, lang, prompts, finish)
})
}
/**
* Handles keyboard-interactive authentication prompts.
* @param {string} name - The name of the authentication request.
* @param {string} instructions - The instructions for the keyboard-interactive prompt.
* @param {string} lang - The language of the prompt.
* @param {Array<Object>} prompts - The list of prompts provided by the server.
* @param {Function} finish - The callback to complete the keyboard-interactive authentication.
*/
handleKeyboardInteractive(name, instructions, lang, prompts, finish) {
debug('handleKeyboardInteractive: Keyboard-interactive auth %O', prompts)
// Check if we should always send prompts to the client
if (this.config.ssh.alwaysSendKeyboardInteractivePrompts) {
this.sendPromptsToClient(name, instructions, prompts, finish)
return
}
const responses = []
let shouldSendToClient = false
for (let i = 0; i < prompts.length; i += 1) {
if (prompts[i].prompt.toLowerCase().includes('password') && this.creds.password) {
responses.push(this.creds.password)
} else {
shouldSendToClient = true
break
}
}
if (shouldSendToClient) {
this.sendPromptsToClient(name, instructions, prompts, finish)
} else {
finish(responses)
}
}
/**
* Sends prompts to the client for keyboard-interactive authentication.
*
* @param {string} name - The name of the authentication method.
* @param {string} instructions - The instructions for the authentication.
* @param {Array<{ prompt: string, echo: boolean }>} prompts - The prompts to be sent to the client.
* @param {Function} finish - The callback function to be called when the client responds.
*/
sendPromptsToClient(name, instructions, prompts, finish) {
this.emit('keyboard-interactive', {
name: name,
instructions: instructions,
prompts: prompts.map((p) => ({ prompt: p.prompt, echo: p.echo })),
})
this.once('keyboard-interactive-response', (responses) => {
finish(responses)
})
}
/**
* Generates the SSH configuration object based on credentials.
* @param {Object} creds - The credentials object containing host, port, username, and optional password.
* @param {Object} creds - The credentials object
* @param {boolean} useKey - Whether to attempt key authentication
* @returns {Object} - The SSH configuration object.
* @returns {Object} - The SSH configuration object
*/
getSSHConfig(creds, useKey) {
const config = {
@ -154,6 +226,16 @@ class SSHConnection extends EventEmitter {
throw new SSHConnectionError('Invalid private key format')
}
config.privateKey = privateKey
// Check if key is encrypted and passphrase is needed
if (this.isEncryptedKey(privateKey)) {
const passphrase = creds.passphrase || this.config.user.passphrase
if (!passphrase) {
throw new SSHConnectionError('Encrypted private key requires a passphrase')
}
debug('Adding passphrase for encrypted private key')
config.passphrase = passphrase
}
} else if (creds.password) {
debug('Using password authentication')
config.password = creds.password

View file

@ -80,7 +80,7 @@ export function getValidatedPort(portInput) {
* - port (number)
* AND either:
* - password (string) OR
* - privateKey/privateKey (string)
* - privateKey (string) with optional passphrase (string)
*
* @param {Object} creds - The credentials object.
* @returns {boolean} - Returns true if the credentials are valid, otherwise false.
@ -97,11 +97,14 @@ export function isValidCredentials(creds) {
return false
}
// Must have either password or privateKey/privateKey
// Must have either password or privateKey
const hasPassword = typeof creds.password === 'string'
const hasPrivateKey = typeof creds.privateKey === 'string' || typeof creds.privateKey === 'string'
const hasPrivateKey = typeof creds.privateKey === 'string'
return hasPassword || hasPrivateKey
// Passphrase is optional but must be string if provided
const hasValidPassphrase = !creds.passphrase || typeof creds.passphrase === 'string'
return (hasPassword || hasPrivateKey) && hasValidPassphrase
}
/**
@ -171,7 +174,9 @@ export function modifyHtml(html, config) {
* @returns {Object} The masked object
*/
export function maskSensitiveData(obj, options) {
const defaultOptions = {}
const defaultOptions = {
properties: ['password', 'privateKey', 'passphrase', 'key', 'secret', 'token'],
}
debug('maskSensitiveData')
const maskingOptions = Object.assign({}, defaultOptions, options || {})

View file

@ -26,7 +26,7 @@
"keepaliveCountMax": 10,
"allowedSubnets": [],
"alwaysSendKeyboardInteractivePrompts": false,
"disableInteractiveAuth": true,
"disableInteractiveAuth": false,
"algorithms": {
"cipher": [
"aes128-ctr",

View file

@ -51,7 +51,7 @@
"lint:fix": "eslint app --fix",
"watch": "NODE_ENV=development DEBUG=webssh* node --watch index.js",
"test": "node --test tests/*.test.js",
"release": "standard-version -a -s --release-as patch --commit-all",
"release": "npm run lint:fix && npm run test && standard-version -a -s --release-as patch --commit-all --infile ChangeLog.md",
"release:dry-run": "standard-version -a -s --release-as patch --dry-run",
"publish:dry-run": "npm publish --dry-run",
"publish:npm": "npm publish",