5.2 KiB
5.2 KiB
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
mkdir ssh-proxy
cd ssh-proxy
npm init -y
2. Install dependencies
npm install express ssh2 cors dotenv
npm install --save-dev @types/express @types/node typescript
3. Create server.ts
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
{
"scripts": {
"dev": "tsx watch server.ts",
"build": "tsc",
"start": "node dist/server.js"
}
}
5. Run locally
npm run dev
6. Deploy (optional)
You can deploy to:
- Fly.io (recommended for low latency)
- Railway
- Render
- Heroku
Example Fly.io deployment:
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
PORT=3001
MAX_CONNECTIONS=100
CONNECTION_TIMEOUT_MS=3600000 # 1 hour
Testing
# 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
- Build and deploy this service
- Update
SSH_PROXY_URLin wrangler.jsonc to point to your deployed proxy - Set
OPENROUTER_API_KEYsecret in Cloudflare Workers - Test the full integration