Add Task #112: Trinity Core Security Hardening
Gemini identified 5 concerns: 1. Command injection (CRITICAL) - Fix: use spawn instead of exec 2. Race conditions (MEDIUM) - Future: server locking 3. SD card wear (HIGH) - FIXED: cron log rotation 4. Cloudflare Access (MEDIUM) - Future enhancement 5. System prompts (LOW) - Document for Catalyst/Orb Updated MCP implementation spec with secure spawn-based SSH execution. Task #112 is a BLOCKER for Task #111 (web MCP deployment). Chronicler #76
This commit is contained in:
@@ -313,9 +313,67 @@ mcpServer.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
}));
|
||||
```
|
||||
|
||||
#### Block E: Execution Logic & SSH Wrapper
|
||||
#### Block E: Execution Logic & SSH Wrapper (SECURE VERSION)
|
||||
|
||||
**IMPORTANT:** Uses `spawn` instead of `exec` to prevent command injection attacks.
|
||||
|
||||
```javascript
|
||||
import { spawn } from 'child_process';
|
||||
|
||||
function executeSSH(server, command) {
|
||||
return new Promise((resolve) => {
|
||||
const target = SERVERS[server];
|
||||
|
||||
log(`EXEC [${server}] ${command}`);
|
||||
|
||||
// spawn with array args prevents shell injection
|
||||
const ssh = spawn('ssh', [
|
||||
'-o', 'ConnectTimeout=10',
|
||||
'-o', 'StrictHostKeyChecking=no',
|
||||
`${target.user}@${target.host}`,
|
||||
command
|
||||
]);
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
ssh.stdout.on('data', (data) => stdout += data.toString());
|
||||
ssh.stderr.on('data', (data) => stderr += data.toString());
|
||||
|
||||
ssh.on('close', (code) => {
|
||||
const success = code === 0;
|
||||
log(`RESULT [${server}] success=${success}`);
|
||||
resolve({
|
||||
success,
|
||||
stdout: stdout.trim(),
|
||||
stderr: stderr.trim(),
|
||||
error: success ? null : `Exit code ${code}`
|
||||
});
|
||||
});
|
||||
|
||||
ssh.on('error', (err) => {
|
||||
log(`ERROR [${server}] ${err.message}`);
|
||||
resolve({
|
||||
success: false,
|
||||
stdout: '',
|
||||
stderr: err.message,
|
||||
error: err.message
|
||||
});
|
||||
});
|
||||
|
||||
// Timeout after 30 seconds
|
||||
setTimeout(() => {
|
||||
ssh.kill();
|
||||
resolve({
|
||||
success: false,
|
||||
stdout: stdout.trim(),
|
||||
stderr: 'Command timed out after 30 seconds',
|
||||
error: 'Timeout'
|
||||
});
|
||||
}, 30000);
|
||||
});
|
||||
}
|
||||
|
||||
mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
if (request.params.name === "list_servers") {
|
||||
const serverList = Object.keys(SERVERS).join(", ");
|
||||
@@ -330,20 +388,13 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
return { content: [{ type: "text", text: `Error: Unknown server ${server}` }], isError: true };
|
||||
}
|
||||
|
||||
log(`EXEC [${server}] ${command}`);
|
||||
const sshCmd = `ssh -o ConnectTimeout=10 ${target.user}@${target.host} "${command.replace(/"/g, '\\"')}"`;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
exec(sshCmd, { timeout: 30000 }, (error, stdout, stderr) => {
|
||||
const success = !error;
|
||||
log(`RESULT [${server}] success=${success}`);
|
||||
|
||||
let output = stdout.trim();
|
||||
if (error || stderr) output += `\nError/Stderr: ${stderr.trim()} ${error?.message || ''}`;
|
||||
|
||||
resolve({ content: [{ type: "text", text: output }], isError: !success });
|
||||
});
|
||||
});
|
||||
const result = await executeSSH(server, command);
|
||||
|
||||
let output = result.stdout || '(no output)';
|
||||
if (result.stderr) output += `\nSTDERR: ${result.stderr}`;
|
||||
if (result.error && !result.success) output += `\nError: ${result.error}`;
|
||||
|
||||
return { content: [{ type: "text", text: output }], isError: !result.success };
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
186
docs/tasks-index/task-112-trinity-core-security.md
Normal file
186
docs/tasks-index/task-112-trinity-core-security.md
Normal file
@@ -0,0 +1,186 @@
|
||||
---
|
||||
task_number: 112
|
||||
title: Trinity Core Security Hardening
|
||||
status: Planned
|
||||
priority: P1-High
|
||||
is_blocker: true
|
||||
owner: Michael
|
||||
tags:
|
||||
- trinity-core
|
||||
- security
|
||||
- infrastructure
|
||||
estimated_hours: 3
|
||||
---
|
||||
|
||||
# Trinity Core Security Hardening
|
||||
|
||||
Address security concerns identified by Gemini before deploying web MCP upgrade.
|
||||
|
||||
## Why This is a Blocker
|
||||
|
||||
Trinity Core provides root SSH access to 7 production servers. Before enabling multi-user access (Holly, Meg) or native Claude.ai integration, we must address these vulnerabilities.
|
||||
|
||||
---
|
||||
|
||||
## Issue 1: Command Injection (CRITICAL)
|
||||
|
||||
**The Problem:**
|
||||
Current code:
|
||||
```javascript
|
||||
const sshCmd = `ssh -o ConnectTimeout=10 ${target.user}@${target.host} "${command.replace(/"/g, '\\"')}"`;
|
||||
exec(sshCmd, ...);
|
||||
```
|
||||
|
||||
This only escapes double quotes. Subshells `$(...)`, backticks `` `...` ``, and environment variables `$VAR` are NOT escaped. A prompt injection attack could execute arbitrary commands.
|
||||
|
||||
**Example Attack:**
|
||||
```
|
||||
Command: echo $(cat /etc/shadow)
|
||||
Result: Dumps password hashes
|
||||
```
|
||||
|
||||
**The Fix:**
|
||||
Use `child_process.spawn` with array arguments instead of `exec` with string interpolation:
|
||||
|
||||
```javascript
|
||||
import { spawn } from 'child_process';
|
||||
|
||||
function executeCommand(server, command) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const target = SERVERS[server];
|
||||
|
||||
// spawn prevents shell interpretation
|
||||
const ssh = spawn('ssh', [
|
||||
'-o', 'ConnectTimeout=10',
|
||||
`${target.user}@${target.host}`,
|
||||
command // passed as single argument, not interpolated into shell
|
||||
]);
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
ssh.stdout.on('data', (data) => stdout += data);
|
||||
ssh.stderr.on('data', (data) => stderr += data);
|
||||
|
||||
ssh.on('close', (code) => {
|
||||
resolve({
|
||||
success: code === 0,
|
||||
stdout: stdout.trim(),
|
||||
stderr: stderr.trim(),
|
||||
code
|
||||
});
|
||||
});
|
||||
|
||||
ssh.on('error', reject);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Status:** ❌ Not implemented — MUST FIX before web MCP deployment
|
||||
|
||||
---
|
||||
|
||||
## Issue 2: AI Concurrency / Race Conditions (MEDIUM)
|
||||
|
||||
**The Problem:**
|
||||
If Chronicler and Catalyst both run commands on the same server simultaneously:
|
||||
- Catalyst restarts Docker
|
||||
- Chronicler deploys container
|
||||
- Chronicler's command fails or corrupts deployment
|
||||
|
||||
**The Fix:**
|
||||
Add server locking to Arbiter:
|
||||
|
||||
```sql
|
||||
CREATE TABLE server_locks (
|
||||
server VARCHAR(50) PRIMARY KEY,
|
||||
locked_by VARCHAR(50),
|
||||
locked_at TIMESTAMP,
|
||||
expires_at TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
Before executing, check lock. If locked, return:
|
||||
*"tx1-dallas is currently locked by The Catalyst. Please try again in a moment."*
|
||||
|
||||
**Status:** ⏳ Future enhancement — not critical for initial deployment
|
||||
|
||||
---
|
||||
|
||||
## Issue 3: SD Card Wear & Log Exhaustion (HIGH)
|
||||
|
||||
**The Problem:**
|
||||
- Pi SD cards have finite write cycles
|
||||
- `fs.appendFileSync` on every command wears the card
|
||||
- No log rotation = disk fills up
|
||||
|
||||
**The Fix (Implemented):**
|
||||
Added cron job to rotate logs daily:
|
||||
```
|
||||
0 0 * * * truncate -s 0 /home/claude_executor/mcp-server/command.log && echo "[$(date -Iseconds)] Log rotated" >> /home/claude_executor/mcp-server/command.log
|
||||
```
|
||||
|
||||
**Better Long-term Fix:**
|
||||
Send logs to Arbiter's PostgreSQL instead of local disk (Task #109 already covers this).
|
||||
|
||||
**Status:** ✅ Mitigated with cron rotation
|
||||
|
||||
---
|
||||
|
||||
## Issue 4: Cloudflare Access Layer (MEDIUM)
|
||||
|
||||
**The Problem:**
|
||||
`mcp.firefrostgaming.com` is public. Malicious requests hit the Pi even if rejected by auth.
|
||||
|
||||
**The Fix:**
|
||||
Enable Cloudflare Access (Zero Trust) for the subdomain:
|
||||
1. Only allow requests from Anthropic IP ranges
|
||||
2. Or require Cloudflare Service Token header
|
||||
3. Drops malicious traffic at edge before reaching Pi
|
||||
|
||||
**Status:** ⏳ Future enhancement — token auth is sufficient for now
|
||||
|
||||
---
|
||||
|
||||
## Issue 5: System Prompt Alignment (LOW)
|
||||
|
||||
**The Problem:**
|
||||
AI partners won't know how to use Trinity Core unless told.
|
||||
|
||||
**The Fix:**
|
||||
Update system prompts / project instructions for:
|
||||
- Chronicler: Already has Trinity Core context in project instructions
|
||||
- Catalyst: Add Trinity Core section to Catalyst project
|
||||
- The Orb: Add Trinity Core section to Orb project
|
||||
|
||||
Include:
|
||||
- Available servers
|
||||
- How to run commands
|
||||
- What requires approval
|
||||
- What to do on timeout
|
||||
|
||||
**Status:** ⏳ Document when Catalyst/Orb projects are created
|
||||
|
||||
---
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
**Before Web MCP Deployment (Task #111):**
|
||||
- [ ] Fix command injection with `spawn` instead of `exec`
|
||||
- [ ] Verify log rotation is working
|
||||
|
||||
**After Initial Deployment:**
|
||||
- [ ] Consider Cloudflare Access
|
||||
- [ ] Consider server locking for concurrency
|
||||
- [ ] Update Catalyst/Orb system prompts when created
|
||||
|
||||
---
|
||||
|
||||
## Related Tasks
|
||||
|
||||
- Task #109: MCP Logging in Trinity Console (moves logs off Pi)
|
||||
- Task #111: Trinity Core Web MCP Connector (main implementation)
|
||||
|
||||
---
|
||||
|
||||
**Fire + Frost + Foundation = Where Love Builds Legacy** 💙🔥❄️
|
||||
Reference in New Issue
Block a user