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。
它只做三件防禦:
- localhost binding gate
- tool allowlist gate
- dependency audit gate
而且要真的跑得動。 如果你能自己跑通,
你就不是只學到概念。 你已經有一個可落地的最小安全骨架。
Learn
這個 lab 在驗什麼
這堂課不是在證明:
- 你已經做出 production-grade MCP server
- 你已經做好完整 auth middleware
- 你已經處理了所有 package supply chain 問題
它要驗的是比較小,
但非常關鍵的 enforcement:
| 驗證點 | 你要看到什麼 |
|---|---|
| safe bind | 127.0.0.1 / localhost / ::1 可以通過 |
| bind reject | 0.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.1localhost::1
如果有人傳:
0.0.0.0192.168.1.2310.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 subprocessrequests.post(...)urllib.request.urlopen(...)https://...~/.ssh.gnupg
這個函式的工作就是把這些訊號找出來, 回傳給呼叫者。
不做結論。 先做盤點。
為什麼這三個 gate 要擺在最前面
因為它們都屬於:
系統還沒開始真正幹活之前, 就該先決定的事。
不是等到:
- server 已經 listen 起來
- tool 已經被調用
- package 已經載進來
才補救。 安全最好的一刻,
通常是事情還沒發生之前。
Reference code:完整可跑的最小安全骨架
下面這段 code 只用 Python 標準函式庫。
它會依序做五件事:
bind_safely("127.0.0.1", 8000),應該成功bind_safely("0.0.0.0", 8000),應該被拒絕validate_tool("search", {"search", "summarize"}),應該成功validate_tool("run_arbitrary_command", {"search", "summarize"}),應該被拒絕- 建立一個臨時可疑 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.x10.x.x.x172.16.x.x
你就已經把需求從 「本機服務」
升級成 「LAN 服務」
那時你就該另外處理:
- auth
- reverse proxy
- VPN
- 文件記錄
而不是偷偷夾帶在一個本來叫 bind_safely 的 helper 裡。
這份 skeleton 真正想教你的,不是三個函式
而是一個工作順序:
- 先拒絕不該存在的 network exposure
- 再拒絕不該出現的 tool capability
- 再盤點你正在信任什麼 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_safely、validate_tool、audit_dependency。要求是0.0.0.0直接拒絕、未授權 tool 直接PermissionError、可疑 dependency 要列出 import / call / URL / sensitive path pattern。請只用 Python 標準函式庫,並附一段可以直接跑的 demo。
Do
互動示範
挑戰任務
請修改 bind_safely(host, port),除了 127.0.0.1、localhost、::1 之外,也接受 Tailscale 使用的 100.64.0.0/10 範圍。請用 bind_safely("100.100.100.100", 8000) 做測試,最後印出回傳值。