2025-10-09 22:03:37 -06:00

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