跳到主要內容
邊界實驗室 · Boundary Lab
正在啟動 Python 環境(首次約 15 秒)...

Lab:Minimal Secure MCP Server

前三課在縮洞,這一課把洞口真的做成 hard fail。

Hook

到這裡, 你其實已經有三塊拼圖。

第一塊來自 Lesson 02:

network surface 不能先對外打開。

第二塊來自 Lesson 03:

tool surface 不能把所有能力都交給 agent。

第三塊來自 Lesson 04:

supply chain 不能因為看起來方便就直接信。

如果這三塊一直分開看, 很容易停在概念層。

你懂:

  • 0.0.0.0 不好
  • 任意 tool 不好
  • 可疑 dependency 不好

但你未必有一段最小可跑的 code, 把三件事接在一起。

這堂 lab 的目標很直接:

做一個最小 MCP-style tool server skeleton。 它不接真 MCP framework。

不接真網路 server。 不接真 agent。

它只做三件防禦:

  1. localhost binding gate
  2. tool allowlist gate
  3. dependency audit gate

而且要真的跑得動。 如果你能自己跑通,

你就不是只學到概念。 你已經有一個可落地的最小安全骨架。

Learn

這個 lab 在驗什麼

這堂課不是在證明:

  • 你已經做出 production-grade MCP server
  • 你已經做好完整 auth middleware
  • 你已經處理了所有 package supply chain 問題

它要驗的是比較小,

但非常關鍵的 enforcement:

驗證點你要看到什麼
safe bind127.0.0.1 / localhost / ::1 可以通過
bind reject0.0.0.0 直接被拒絕
tool allowlist合法 tool 會被接受
tool reject未授權 tool 直接噴 PermissionError
dependency audit可疑 package 會被列出警告 pattern

做到這五件事,

就已經足夠證明:

attack surface 不是你心裡知道不行。 而是 code 裡真的有 gate。

我們只需要三個 function

這份 lab 的骨架刻意很小。 只保留三個核心 function。

def bind_safely(host: str, port: int) -> tuple[str, int]:
    """只允許 127.0.0.1 / localhost / ::1,其他 raise"""
    ...

def validate_tool(tool_name: str, allowlist: set[str]) -> None:
    """tool_name 不在 allowlist 直接 raise PermissionError"""
    ...

def audit_dependency(package_path: str) -> list[str]:
    """scan package source,回傳可疑 pattern 清單"""
    ...

你可以把它理解成:

  • 第一個 function 守 network surface
  • 第二個 function 守 tool surface
  • 第三個 function 守 trust delegation / supply chain surface

這三個一起擺在最外層, 就是最小 attack-surface gate。

bind_safely(...) 在做什麼

這個函式的職責很單純。 它不是幫你找最佳位址。

它只做一件事:

對不該被接受的 host 明確 fail。

允許名單只有:

  • 127.0.0.1
  • localhost
  • ::1

如果有人傳:

  • 0.0.0.0
  • 192.168.1.23
  • 10.0.0.8

你不要默默接受。 直接 raise。

這就是 default deny。

validate_tool(...) 在做什麼

很多人會把 allowlist 想成配置檔細節。 不是。

在 agent 系統裡, tool allowlist

就是權限模型本體。

這個 lab 用最小版本表達:

如果 tool_name 不在 allowlist

就立刻丟出 PermissionError。 這個規則故意很硬。

因為一旦你改成:

「先跑看看,不行再記 log」 你的 system boundary

就已經輸一半了。

audit_dependency(...) 在做什麼

它不是完整 SCA 平台。 它只是最小 heuristic scanner。

但這種掃描在工程現場很有價值。 因為很多可疑 dependency

在 source 上其實就有很明顯的訊號:

  • import subprocess
  • requests.post(...)
  • urllib.request.urlopen(...)
  • https://...
  • ~/.ssh
  • .gnupg

這個函式的工作就是把這些訊號找出來, 回傳給呼叫者。

不做結論。 先做盤點。

為什麼這三個 gate 要擺在最前面

因為它們都屬於:

系統還沒開始真正幹活之前, 就該先決定的事。

不是等到:

  • server 已經 listen 起來
  • tool 已經被調用
  • package 已經載進來

才補救。 安全最好的一刻,

通常是事情還沒發生之前。

Reference code:完整可跑的最小安全骨架

下面這段 code 只用 Python 標準函式庫。

它會依序做五件事:

  1. bind_safely("127.0.0.1", 8000),應該成功
  2. bind_safely("0.0.0.0", 8000),應該被拒絕
  3. validate_tool("search", {"search", "summarize"}),應該成功
  4. validate_tool("run_arbitrary_command", {"search", "summarize"}),應該被拒絕
  5. 建立一個臨時可疑 dependency,跑 audit_dependency(...) 並印出警告清單

你應該看到的輸出

如果一切正常,

你至少會看到這個結構:

bind_ok = ('127.0.0.1', 8000)
bind_rejected = only loopback hosts are allowed: 0.0.0.0
tool_ok = search
tool_rejected = tool not allowed: run_arbitrary_command
audit_warnings =
call:requests.post
call:subprocess.run
import:requests
import:subprocess
path:~/.ssh
url:https://evil.example/upload

這段輸出之所以重要, 不是因為它很漂亮。

而是你能明確看到:

  • 不安全 host 被拒絕
  • 未授權 tool 被拒絕
  • 可疑 dependency 被標記

三種 gate 都真的活著。

為什麼 audit_dependency(...) 先回傳 list,而不是直接 block

因為現實世界裡, 不是每個 requests.post

都代表惡意。 有些 package

本來就合法需要 outbound request。

所以比較合理的分工是:

  • scanner 負責回報訊號
  • policy layer 決定哪些訊號該 block

這也是為什麼本 lab 的函式名稱叫 audit 不是 ban

它先做 evidence collection。

為什麼 validate_tool(...) 要用 allowlist,不用 denylist

因為 denylist 的思維是:

先假設大部分東西都可以, 再列出你想到的不行項目。

這跟整門課的 default deny 完全相反。

tool surface 最好的起點永遠是:

先只有必要的工具。 之後每加一個,

都當成一次顯式授權。

為什麼 bind_safely(...) 不接受私網 IP

因為這堂 lab 要教的是最保守預設。

只要你今天接受:

  • 192.168.x.x
  • 10.x.x.x
  • 172.16.x.x

你就已經把需求從 「本機服務」

升級成 「LAN 服務」

那時你就該另外處理:

  • auth
  • reverse proxy
  • VPN
  • 文件記錄

而不是偷偷夾帶在一個本來叫 bind_safely 的 helper 裡。

這份 skeleton 真正想教你的,不是三個函式

而是一個工作順序:

  1. 先拒絕不該存在的 network exposure
  2. 再拒絕不該出現的 tool capability
  3. 再盤點你正在信任什麼 dependency

這個順序很合理,

因為它剛好對應:

  • 外層入口
  • 執行能力
  • 供應鏈來源

也就是 attack surface 三維。

從這裡往 production 走,下一步會是什麼

如果你想把它往真系統推,

下一步通常會加:

  • HTTP auth / signed request
  • structured audit log
  • allowlist config file
  • quarantine policy for suspicious deps
  • CI 裡的 dependency scan

但這些是下一步。 在做那些之前,

先把最小 gate 寫出來更重要。

只要這段 code 跑得動,你就已經超過很多系統

因為很多真實服務的狀態其實是:

  • 0.0.0.0 沒人在管
  • tool 全開
  • dependency 只看能不能安裝成功

而這份 lab 至少做了三件對的事:

  • fail closed
  • 最小 allowlist
  • 先盤點 supply chain 訊號

這就是一個系列收尾課 該留下的最小骨架。

AI 協作:學了這個,跟 AI 怎麼配合?

這個技能在 AI 協作中的定位, 是讓你要求 AI 先幫你搭出最小安全骨架,

再由你把它接到真實 MCP server 或內部 tool API。 你的人類優勢:

  • 只有你知道哪些 host 是你環境裡真的合理例外,哪些只是圖方便留下來的慣性設定。
  • 只有你能決定哪幾個 tool 真的是核心能力,值得進 allowlist,其餘都應該先關閉。

可以這樣跟 AI 說:

請幫我做一個最小 secure MCP-style server skeleton。我要三個 function:bind_safelyvalidate_toolaudit_dependency。要求是 0.0.0.0 直接拒絕、未授權 tool 直接 PermissionError、可疑 dependency 要列出 import / call / URL / sensitive path pattern。請只用 Python 標準函式庫,並附一段可以直接跑的 demo。

Do

互動示範

DEMO 1可以修改程式碼試玩

挑戰任務

Task 1

請修改 bind_safely(host, port),除了 127.0.0.1localhost::1 之外,也接受 Tailscale 使用的 100.64.0.0/10 範圍。請用 bind_safely("100.100.100.100", 8000) 做測試,最後印出回傳值。

BackTake the Exam →