# 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() // 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":"","command":"ls -la"}' # Disconnect curl -X POST http://localhost:3001/ssh/disconnect \ -H "Content-Type: application/json" \ -d '{"connectionId":""}' ``` ## 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