Whitelist hay blocklist cho MCP shell? Kiến trúc bảo mật của aki-mcp-sv
MCP server mở shell ra internet phải chọn giữa whitelist và blocklist. So sánh aki-mcp-sv với Desktop Commander, mcp-remote, server-filesystem chính thức của Anthropic, và phân tích OAuth 2.1 không DCR.
Một MCP (Model Context Protocol) server muốn cho Claude web hoặc ChatGPT chạy lệnh shell trên máy thật phải trả lời một câu hỏi kiến trúc trước tiên: chặn lệnh bằng whitelist hay blocklist? aki-mcp-sv, MCP (Model Context Protocol) server mã nguồn mở của Lạc Việt Anh, chọn whitelist. Bài này giải thích vì sao lựa chọn đó đúng cho một server tự host ra internet, và đặt nó cạnh các pattern MCP remote khác đang tồn tại.
Bốn nhóm MCP server, và aki-mcp-sv thuộc nhóm nào
Nhìn rộng ra, phần lớn MCP server hiện có rơi vào ba nhóm không giải quyết cùng bài toán với aki-mcp-sv, và aki-mcp-sv là nhóm thứ tư:
- ◆Local-only, không remote: Desktop Commander (blocklist, chạy stdio cho Claude Desktop) và server-filesystem chính thức của Anthropic (không có shell tool nào, chỉ đọc/ghi file trong thư mục được phép) — cả hai không thiết kế để lộ ra internet.
- ◆Bridge/proxy, không phải server: mcp-remote (geelen) là công cụ phía client, chuyển tiếp stdio↔HTTP/SSE tới một remote MCP server đã có sẵn ở nơi khác; bản thân nó không cấp filesystem hay shell nào.
- ◆SaaS cloud-hosted: Composio, Smithery-hosted MCP chạy trên hạ tầng của nhà cung cấp, làm trung gian gọi API bên thứ 3 (GitHub, Slack...), không chạm vào filesystem/shell máy cá nhân bạn.
- ◆Tự host, tự remote (aki-mcp-sv): chạy trên chính máy bạn, tự expose qua Tailscale Funnel, bạn giữ toàn bộ quyền kiểm soát hạ tầng thay vì giao cho bên thứ 3.
Whitelist thắng blocklist ở đâu
Desktop Commander chặn shell bằng blocklist (blockedCommands): liệt kê lệnh cấm, mặc định cho phép. Một blocklist về bản chất luôn hở, không thể liệt kê hết mọi lệnh nguy hiểm và biến thể của nó. Guide chính chủ của Desktop Commander cũng nói thẳng: không bao giờ nên lộ nó ra internet.
aki-mcp-sv nhắm vào một tình huống khác: mở truy cập cho Claude và ChatGPT trên web, qua internet mở bằng Tailscale Funnel. Lựa chọn ngược lại, whitelist với mặc định từ chối, mang lại bốn thuộc tính: fail-safe (lệnh lạ tự động bị chặn), bề mặt tấn công tối thiểu, chi tiết tới subcommand (git chỉ được scope ở status/log/diff/show), và read-only mặc định.
Thuộc tính “read-only mặc định” này không chỉ là lý thuyết: bản 1.1.0 vẫn giữ find và sort trong allowlist mặc định, dù flag riêng của hai lệnh phá vỡ read-only (find -delete/-exec, sort -o <path>), và execFile không chặn được vì nguy hiểm nằm ở chính argv của binary chứ không phải ở shell. Bản 1.2.0 đóng lỗ hổng này (issue #2) bằng cách bỏ hẳn hai lệnh khỏi mặc định thay vì vá từng flag, đúng triết lý whitelist: curate bề mặt, không patch từng trường hợp. find_path/search_content của arm search thay đúng nhu cầu tra cứu read-only mà hai lệnh đó từng phục vụ.
OAuth 2.1: Claude và Gemini dán tay, ChatGPT và Grok tự đăng ký
claude.ai mặc định thử tự đăng ký client (Dynamic Client Registration - DCR) trước khi kết nối. aki-mcp-sv không quảng cáo endpoint đó cho Claude; client_id/client_secret được sinh một lần khi chạy npm start, và người dùng dán tay vào ô Advanced settings, đúng cơ chế pre-registered client credentials mà tài liệu của Anthropic công nhận là cách hợp lệ để bỏ qua DCR. Gemini dùng chung cơ chế dán tay này: nó tái sử dụng đúng client confidential của Claude (cùng Client ID/Secret), không tự đăng ký.
ChatGPT và Grok thì ngược lại: cả hai tự đăng ký qua POST /register (RFC 7591) làm public client (token_endpoint_auth_method: none), với redirect URI riêng của từng bên (chatgpt.com, grok.com/connectors-oauth-exchange-code/) được allowlist sẵn. Cả ba luồng dán tay lẫn tự đăng ký đều phải qua màn hình passphrase và PKCE trước khi nhận token; đây là điểm khác với mcp-remote, nơi toàn bộ OAuth 2.1 + PKCE + DCR chỉ xảy ra ở phía client để nói chuyện với một remote server khác, không phải cơ chế tự bảo vệ của chính server đó.
Hai lớp thực sự chặn truy cập trái phép: passphrase 10 ký tự tại /authorize (~50 bit entropy, không dùng nút Approve trần vì /authorize là endpoint public), cộng PKCE S256 để access token chỉ cấp cho đúng client giữ code_verifier khớp.
Cơ chế thực thi: execFile, không qua shell thật
shell-mcp.js thực thi lệnh bằng execFile, không bao giờ đi qua shell thật, nên các ký tự nối lệnh như dấu chấm phẩy, and-and, pipe bị chặn ở tầng thực thi chứ không phải bằng lọc chuỗi. gatekeeper.js là cổng public duy nhất; mcp-hub thật chỉ nghe ở loopback nên API quản trị không xác thực của nó (/api/*) không bao giờ lộ ra internet.
Giới hạn
Không xoay vòng refresh token cho Claude, chấp nhận được vì đây là confidential client (có client_secret). Restart npm start mất toàn bộ session đang cấp vì token chỉ sống trong RAM. Không rate-limit /authorize, chấp nhận được nhờ entropy ~50 bit của passphrase khiến brute-force không khả thi. Gemini xác thực OAuth thành công nhưng qua kiểm thử thực tế 09/08/2026 chưa điều khiển tool MCP ổn định (kết nối khoẻ, gọi tool không đáng tin); Claude, ChatGPT và Grok mới là các client đáng tin cậy hiện tại.