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 ## Detailed Changes
### 1. Authentication Options ### 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 - Removed `user.overridebasic` option
- Added keyboard-interactive authentication controls - Added keyboard-interactive authentication controls
@ -128,7 +128,8 @@ These settings are now managed client-side.
"user": { "user": {
"name": null, "name": null,
"password": null, "password": null,
"privateKey": null "privateKey": null,
"passphrase": null
}, },
"ssh": { "ssh": {
"host": null, "host": null,
@ -138,7 +139,39 @@ These settings are now managed client-side.
"keepaliveInterval": 120000, "keepaliveInterval": 120000,
"keepaliveCountMax": 10, "keepaliveCountMax": 10,
"algorithms": { "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": { "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. 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> <a name="0.2.22"></a>
## [0.2.22](https://github.com/billchurch/WebSSH2/compare/v0.2.21...v0.2.22) (2024-11-30) ## [0.2.22](https://github.com/billchurch/WebSSH2/compare/v0.2.21...v0.2.22) (2024-11-30)
@ -539,4 +579,4 @@ Mostly client (browser) related changes in this release
### Added ### Added
- Initial proof of concept and release. For historical purposes only. - Initial proof of concept and release. For historical purposes only.

View file

@ -149,7 +149,7 @@ If you encounter issues:
4. Verify the ports (3000 and 2222) are available 4. Verify the ports (3000 and 2222) are available
5. Clear browser cache if changes aren't reflecting 5. Clear browser cache if changes aren't reflecting
## Building for Production ## Building for Production (client)
When ready to build for production: 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 ### 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 #### Configuration
@ -215,13 +215,17 @@ Private key authentication can only be configured through the `config.json` file
#### Key Requirements #### Key Requirements
- Only `ssh-rsa` type keys are supported - 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 private key must be in PEM format
- The key in `config.json` must be on a single line with `\n` as line separators - The key in `config.json` must be on a single line with `\n` as line separators
- Must include the appropriate header and footer: - Must include the appropriate header and footer:
``` ```
-----BEGIN RSA PRIVATE KEY-----\n[... key content ...]\n-----END RSA PRIVATE KEY----- -----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 #### Generating a Private Key
To generate a new SSH private key, you can use the following command: 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 #### 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 ```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: This command:

View file

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

View file

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

View file

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

View file

@ -3,20 +3,27 @@
import validator from 'validator' import validator from 'validator'
import { EventEmitter } from 'events' import { EventEmitter } from 'events'
import SSHConnection from './ssh.js'
import { createNamespacedDebug } from './logger.js' import { createNamespacedDebug } from './logger.js'
import { SSHConnectionError, handleError } from './errors.js' import { SSHConnectionError, handleError } from './errors.js'
const debug = createNamespacedDebug('socket')
import { isValidCredentials, maskSensitiveData, validateSshTerm } from './utils.js' import { isValidCredentials, maskSensitiveData, validateSshTerm } from './utils.js'
import { MESSAGES } from './constants.js' import { MESSAGES } from './constants.js'
const debug = createNamespacedDebug('socket')
class WebSSH2Socket extends EventEmitter { 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() super()
this.socket = socket this.socket = socket
this.config = config this.config = config
this.ssh = new SSHConnection(config) this.SSHConnectionClass = SSHConnectionClass
this.ssh = null
this.sessionState = { this.sessionState = {
authenticated: false, authenticated: false,
username: null, username: null,
@ -58,10 +65,6 @@ class WebSSH2Socket extends EventEmitter {
this.socket.emit('authentication', { action: 'request_auth' }) this.socket.emit('authentication', { action: 'request_auth' })
} }
this.ssh.on('keyboard-interactive', (data) => {
this.handleKeyboardInteractive(data)
})
this.socket.on('authenticate', (creds) => { this.socket.on('authenticate', (creds) => {
this.handleAuthenticate(creds) this.handleAuthenticate(creds)
}) })
@ -129,6 +132,14 @@ class WebSSH2Socket extends EventEmitter {
creds.privateKey = this.config.user.privateKey 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 this.ssh
.connect(creds) .connect(creds)
.then(() => { .then(() => {
@ -342,6 +353,6 @@ class WebSSH2Socket extends EventEmitter {
} }
} }
export default function (io, config) { export default function (io, config, SSHConnectionClass) {
io.on('connection', (socket) => new WebSSH2Socket(socket, config)) 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 * @param {string} key - The private key string to validate
* @returns {boolean} - Whether the key appears to be valid * @returns {boolean} - Whether the key appears to be valid
*/ */
validatePrivateKey(key) { 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?$/ /^-----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) 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. * 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 * @param {boolean} useKey - Whether to attempt key authentication
* @returns {Object} - The SSH configuration object. * @returns {Object} - The SSH configuration object
*/ */
getSSHConfig(creds, useKey) { getSSHConfig(creds, useKey) {
const config = { const config = {
@ -154,6 +226,16 @@ class SSHConnection extends EventEmitter {
throw new SSHConnectionError('Invalid private key format') throw new SSHConnectionError('Invalid private key format')
} }
config.privateKey = privateKey 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) { } else if (creds.password) {
debug('Using password authentication') debug('Using password authentication')
config.password = creds.password config.password = creds.password

View file

@ -80,7 +80,7 @@ export function getValidatedPort(portInput) {
* - port (number) * - port (number)
* AND either: * AND either:
* - password (string) OR * - password (string) OR
* - privateKey/privateKey (string) * - privateKey (string) with optional passphrase (string)
* *
* @param {Object} creds - The credentials object. * @param {Object} creds - The credentials object.
* @returns {boolean} - Returns true if the credentials are valid, otherwise false. * @returns {boolean} - Returns true if the credentials are valid, otherwise false.
@ -97,11 +97,14 @@ export function isValidCredentials(creds) {
return false return false
} }
// Must have either password or privateKey/privateKey // Must have either password or privateKey
const hasPassword = typeof creds.password === 'string' 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 * @returns {Object} The masked object
*/ */
export function maskSensitiveData(obj, options) { export function maskSensitiveData(obj, options) {
const defaultOptions = {} const defaultOptions = {
properties: ['password', 'privateKey', 'passphrase', 'key', 'secret', 'token'],
}
debug('maskSensitiveData') debug('maskSensitiveData')
const maskingOptions = Object.assign({}, defaultOptions, options || {}) const maskingOptions = Object.assign({}, defaultOptions, options || {})

View file

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

View file

@ -51,7 +51,7 @@
"lint:fix": "eslint app --fix", "lint:fix": "eslint app --fix",
"watch": "NODE_ENV=development DEBUG=webssh* node --watch index.js", "watch": "NODE_ENV=development DEBUG=webssh* node --watch index.js",
"test": "node --test tests/*.test.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", "release:dry-run": "standard-version -a -s --release-as patch --dry-run",
"publish:dry-run": "npm publish --dry-run", "publish:dry-run": "npm publish --dry-run",
"publish:npm": "npm publish", "publish:npm": "npm publish",