245 lines
5.2 KiB
Markdown
245 lines
5.2 KiB
Markdown
# SSH Proxy Service for Bandit Runner
|
|
|
|
This is a standalone Node.js HTTP server that provides SSH connectivity for the Bandit Runner agent running in Cloudflare Workers.
|
|
|
|
## Why is this needed?
|
|
|
|
Cloudflare Workers have limited SSH support (no native SSH client libraries), so we use an external HTTP proxy to handle SSH connections.
|
|
|
|
## Setup
|
|
|
|
### 1. Create a new Node.js project
|
|
|
|
```bash
|
|
mkdir ssh-proxy
|
|
cd ssh-proxy
|
|
npm init -y
|
|
```
|
|
|
|
### 2. Install dependencies
|
|
|
|
```bash
|
|
npm install express ssh2 cors dotenv
|
|
npm install --save-dev @types/express @types/node typescript
|
|
```
|
|
|
|
### 3. Create `server.ts`
|
|
|
|
```typescript
|
|
import express from 'express'
|
|
import { Client } from 'ssh2'
|
|
import cors from 'cors'
|
|
|
|
const app = express()
|
|
app.use(cors())
|
|
app.use(express.json())
|
|
|
|
// Store active connections
|
|
const connections = new Map<string, Client>()
|
|
|
|
// POST /ssh/connect
|
|
app.post('/ssh/connect', async (req, res) => {
|
|
const { host, port, username, password, testOnly } = req.body
|
|
|
|
// Security: Only allow connections to Bandit server
|
|
if (host !== 'bandit.labs.overthewire.org' || port !== 2220) {
|
|
return res.status(403).json({
|
|
success: false,
|
|
message: 'Only connections to bandit.labs.overthewire.org:2220 are allowed'
|
|
})
|
|
}
|
|
|
|
const client = new Client()
|
|
const connectionId = `conn-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
|
|
|
client.on('ready', () => {
|
|
if (testOnly) {
|
|
client.end()
|
|
return res.json({
|
|
connectionId: null,
|
|
success: true,
|
|
message: 'Password validated successfully'
|
|
})
|
|
}
|
|
|
|
connections.set(connectionId, client)
|
|
res.json({
|
|
connectionId,
|
|
success: true,
|
|
message: 'Connected successfully'
|
|
})
|
|
})
|
|
|
|
client.on('error', (err) => {
|
|
res.status(400).json({
|
|
connectionId: null,
|
|
success: false,
|
|
message: `Connection failed: ${err.message}`
|
|
})
|
|
})
|
|
|
|
client.connect({
|
|
host,
|
|
port,
|
|
username,
|
|
password,
|
|
readyTimeout: 10000,
|
|
})
|
|
})
|
|
|
|
// POST /ssh/exec
|
|
app.post('/ssh/exec', async (req, res) => {
|
|
const { connectionId, command, timeout = 30000 } = req.body
|
|
const client = connections.get(connectionId)
|
|
|
|
if (!client) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: 'Connection not found'
|
|
})
|
|
}
|
|
|
|
let output = ''
|
|
let stderr = ''
|
|
|
|
const timeoutHandle = setTimeout(() => {
|
|
res.json({
|
|
output: output + '\n[Command timed out]',
|
|
exitCode: 124,
|
|
success: false,
|
|
duration: timeout,
|
|
})
|
|
}, timeout)
|
|
|
|
client.exec(command, (err, stream) => {
|
|
if (err) {
|
|
clearTimeout(timeoutHandle)
|
|
return res.status(500).json({
|
|
success: false,
|
|
error: err.message
|
|
})
|
|
}
|
|
|
|
stream.on('data', (data: Buffer) => {
|
|
output += data.toString()
|
|
})
|
|
|
|
stream.stderr.on('data', (data: Buffer) => {
|
|
stderr += data.toString()
|
|
})
|
|
|
|
stream.on('close', (code: number) => {
|
|
clearTimeout(timeoutHandle)
|
|
res.json({
|
|
output: output || stderr,
|
|
exitCode: code,
|
|
success: code === 0,
|
|
duration: Date.now() % timeout,
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
// POST /ssh/disconnect
|
|
app.post('/ssh/disconnect', (req, res) => {
|
|
const { connectionId } = req.body
|
|
const client = connections.get(connectionId)
|
|
|
|
if (client) {
|
|
client.end()
|
|
connections.delete(connectionId)
|
|
res.json({ success: true, message: 'Disconnected' })
|
|
} else {
|
|
res.status(404).json({ success: false, message: 'Connection not found' })
|
|
}
|
|
})
|
|
|
|
// GET /ssh/health
|
|
app.get('/ssh/health', (req, res) => {
|
|
res.json({
|
|
status: 'ok',
|
|
activeConnections: connections.size
|
|
})
|
|
})
|
|
|
|
const PORT = process.env.PORT || 3001
|
|
app.listen(PORT, () => {
|
|
console.log(`SSH Proxy running on port ${PORT}`)
|
|
})
|
|
```
|
|
|
|
### 4. Add to `package.json`
|
|
|
|
```json
|
|
{
|
|
"scripts": {
|
|
"dev": "tsx watch server.ts",
|
|
"build": "tsc",
|
|
"start": "node dist/server.js"
|
|
}
|
|
}
|
|
```
|
|
|
|
### 5. Run locally
|
|
|
|
```bash
|
|
npm run dev
|
|
```
|
|
|
|
### 6. Deploy (optional)
|
|
|
|
You can deploy to:
|
|
- **Fly.io** (recommended for low latency)
|
|
- **Railway**
|
|
- **Render**
|
|
- **Heroku**
|
|
|
|
Example Fly.io deployment:
|
|
|
|
```bash
|
|
fly launch
|
|
fly deploy
|
|
```
|
|
|
|
## Security Notes
|
|
|
|
- The proxy hardcodes the allowed SSH target to `bandit.labs.overthewire.org:2220`
|
|
- No other SSH connections are permitted
|
|
- Connection pooling with timeout cleanup (implement auto-cleanup after 1 hour)
|
|
- Rate limiting should be added for production
|
|
|
|
## Environment Variables
|
|
|
|
```bash
|
|
PORT=3001
|
|
MAX_CONNECTIONS=100
|
|
CONNECTION_TIMEOUT_MS=3600000 # 1 hour
|
|
```
|
|
|
|
## Testing
|
|
|
|
```bash
|
|
# Test connection
|
|
curl -X POST http://localhost:3001/ssh/connect \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"host":"bandit.labs.overthewire.org","port":2220,"username":"bandit0","password":"bandit0"}'
|
|
|
|
# Test command execution
|
|
curl -X POST http://localhost:3001/ssh/exec \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"connectionId":"<ID_FROM_ABOVE>","command":"ls -la"}'
|
|
|
|
# Disconnect
|
|
curl -X POST http://localhost:3001/ssh/disconnect \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"connectionId":"<ID>"}'
|
|
```
|
|
|
|
## Next Steps
|
|
|
|
1. Build and deploy this service
|
|
2. Update `SSH_PROXY_URL` in wrangler.jsonc to point to your deployed proxy
|
|
3. Set `OPENROUTER_API_KEY` secret in Cloudflare Workers
|
|
4. Test the full integration
|
|
|