Arsitektur Remote Agent ke Local Repo
Menghubungkan remote AI agent (seperti OpenAI ChatGPT Developer Mode atau Anthropic custom client) langsung ke mesin lokal tanpa proteksi membuka risiko intrusi jaringan dan pembacaan arbitrary files. Model Context Protocol (MCP) menstandarkan pertukaran context melalui JSON-RPC, namun membutuhkan transport layer yang aman saat agent berada di luar local network.
Pola arsitektur bridge menggunakan Cloudflare Tunnel mengeliminasi kebutuhan port-forwarding router dan IP publik statis. Alur komunikasi data berjalan secara terenkripsi:
[Remote AI Agent]
│ (HTTPS + Cloudflare Service Token Header)
▼
[Cloudflare Edge / Cloudflare Access] (Validasi Token Service Auth)
│ (Tunnel Terenkripsi / QUIC/HTTP2)
▼
[cloudflared daemon] (Berjalan di host lokal)
│ (Reverse Proxy ke loopback)
▼
[Local MCP Server : 8080] (Validasi Path Boundary + Sanitasi CLI)
│
▼
[Target Git Repository / Local Filesystem]Implementasi Minimal Server MCP (Node.js)
Implementasi ini menggunakan transport Server-Sent Events (SSE) via @modelcontextprotocol/sdk dan express. Server mengekspos dua tools: inspeksi file lokal dan inspeksi status Git.
// package.json dependencies: @modelcontextprotocol/sdk, express, zod
import express from 'express';
import path from 'node:path';
import fs from 'node:fs/promises';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import { z } from 'zod';
const execFileAsync = promisify(execFile);
const REPO_ROOT = path.resolve(process.env.TARGET_REPO || process.cwd());
// 1. Guard Path Traversal Boundary
function resolveSafePath(userPath) {
const resolved = path.resolve(REPO_ROOT, userPath);
const rel = path.relative(REPO_ROOT, resolved);
if (rel.startsWith('..') || path.isAbsolute(rel)) {
throw new Error('Akses ditolak: Jalur berada di luar direktori repositori.');
}
return resolved;
}
const server = new McpServer({
name: 'local-repo-bridge',
version: '1.0.0'
});
// Tool: Baca isi file
server.tool(
'read_file',
'Membaca konten file dalam batas repository',
{ relative_path: z.string() },
async ({ relative_path }) => {
try {
const safePath = resolveSafePath(relative_path);
const content = await fs.readFile(safePath, 'utf-8');
return { content: [{ type: 'text', text: content }] };
} catch (err) {
return { isError: true, content: [{ type: 'text', text: err.message }] };
}
}
);
// Tool: Git Diff
server.tool(
'git_diff',
'Mengambil git diff repositori target',
{ staged: z.boolean().default(false) },
async ({ staged }) => {
try {
const args = ['diff'];
if (staged) args.push('--staged');
// Eksekusi git menggunakan execFile, bukan exec shell
const { stdout } = await execFileAsync('git', args, {
cwd: REPO_ROOT,
maxBuffer: 10 * 1024 * 1024
});
return { content: [{ type: 'text', text: stdout || 'Tidak ada perubahan.' }] };
} catch (err) {
return { isError: true, content: [{ type: 'text', text: err.message }] };
}
}
);
const app = express();
let transport = null;
app.get('/sse', async (req, res) => {
transport = new SSEServerTransport('/message', res);
await server.connect(transport);
});
app.post('/message', async (req, res) => {
if (transport) {
await transport.handlePostMessage(req, res);
} else {
res.status(400).send('Sesi SSE belum aktif');
}
});
const PORT = process.env.PORT || 8080;
app.listen(PORT, '127.0.0.1', () => {
console.log(`MCP Server berjalan di 127.0.0.1:${PORT} untuk repo: ${REPO_ROOT}`);
});Konfigurasi Cloudflare Tunnel dengan Service Auth
Membuka port server lokal ke internet publik melalui Cloudflare Tunnel tanpa autentikasi akan mengekspos endpoint tool execution ke bot crawling. Gunakan Cloudflare Access dengan aturan Service Token.
1. Inisialisasi Tunnel
Jalankan CLI cloudflared untuk mendaftarkan tunnel baru:
cloudflared tunnel create mcp-repo-tunnel
cloudflared tunnel route dns mcp-repo-tunnel mcp.internal-domain.com2. Konfigurasi config.yml
Buat file konfigurasi tunnel di ~/.cloudflared/config.yml:
tunnel: <TUNNEL_UUID>
credentials-file: /home/user/.cloudflared/<TUNNEL_UUID>.json
ingress:
- hostname: mcp.internal-domain.com
service: http://127.0.0.1:8080
originRequest:
noTLSVerify: false
- service: http_status:404Jalankan daemon tunnel:
cloudflared tunnel run mcp-repo-tunnel3. Penerapan Cloudflare Access Rule
- Buka Cloudflare Zero Trust Dashboard > Access > Service Auth > Create Service Token. Catat
Client IDdanClient Secret. - Navigasi ke Applications > Add an Application > Pilih Self-hosted.
- Masukkan domain aplikasi:
mcp.internal-domain.com. - Buat Access Policy dengan Action: Service Auth dan Rule Type: Service Token. Pilih nama token yang dibuat di langkah 1.
Setiap request tanpa header CF-Access-Client-Id dan CF-Access-Client-Secret akan diblokir dengan status HTTP 403 di edge Cloudflare sebelum menyentuh tunnel lokal.
Mitigasi Keamanan Wajib
1. Path Traversal Guard
Penggunaan raw path dari LLM rawan serangan ../ untuk membaca /etc/passwd atau ~/.ssh/id_rsa. Validasi canonical path mutlak dilakukan:
- Gunakan
path.resolve(REPO_ROOT, input)untuk mendapatkan absolut path. - Evaluasi jalur relatif dengan
path.relative(REPO_ROOT, resolvedPath). Jika string hasil evaluasi diawali..atau berbentuk absolute root, tolak request. - Gunakan
fs.realpathjika ada kebutuhan mendeteksi symbolic link yang keluar dari root directory.
2. Sanitasi Command Execution
Jangan pernah memanggil child_process.exec menggunakan input arbitrary karena parsing shell string membuka celah code injection (misal: ; rm -rf /). Gunakan selalu child_process.execFile dengan array argumen statis:
// Salah (Shell execution):
// exec(`git diff ${userInput}`) -> Rentan injection
// Benar (Direct binary execution):
execFile('git', ['diff', '--', targetFile], { cwd: REPO_ROOT });3. Boundary Allowlist & Denylist
Blokir pembacaan direktori dan file sensitif secara eksplisit di dalam server MCP:
- Denylist wajib:
.git/,.env,*.pem,*.key,node_modules/. - Bila memungkinkan, gunakan read-only filesystem mode untuk memastikan agent tidak dapat memodifikasi file tanpa konfirmasi eksplisit dari operator.
Checklist Verifikasi Koneksi End-to-End
- Verifikasi Blokir Unauthorized (Edge Test):
curl -I https://mcp.internal-domain.com/sseRespons harus menghasilkan status
HTTP/2 403 Forbidden. - Verifikasi Autentikasi Service Token:
curl -N -H "CF-Access-Client-Id: <CLIENT_ID>" \ -H "CF-Access-Client-Secret: <CLIENT_SECRET>" \ https://mcp.internal-domain.com/sseRespons harus mengembalikan header
content-type: text/event-streamdengan endpoint event/message. - Verifikasi Path Guard (Traversal Test):
# Kirim JSON-RPC tool call read_file untuk ../../../etc/passwd # Server wajib mengembalikan flag isError: true - Verifikasi Remote Agent Integration:
Konfigurasikan remote client MCP dengan URL
https://mcp.internal-domain.com/ssedan daftarkan kedua custom header autentikasi Cloudflare Access. Lakukan instruksi natural language: "Cek git diff terakhir dari repositori" dan pastikan toolgit_difftereksekusi di mesin lokal.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!