起因:一個旗標,讓 AI 一秒喊出了錯的資安結論
那天清掉一顆「砍了 EC2(Elastic Compute Cloud,AWS 的雲端虛擬機器)卻忘了收」的閒置 Elastic IP(彈性 IP — AWS 可保留、可重複指派的固定公網位址)後,我順手請我的 AI 助手盤點帳號還開著哪些服務。掃到一台正式環境的 RDS(Relational Database Service,AWS 託管的關聯式資料庫)PostgreSQL,設定裡寫著 PubliclyAccessible: true。
AI 第一個反應就回我:「正式資料庫直接掛公網,有資安風險。」這句話聽起來很合理——但它是錯的。真相要等到實際讀了 Security Group 規則、再從本機跑一次連線測試才浮現。
這篇文章講的就是這個落差:一個旗標的字面意思,跟它實際造成的曝露,是兩回事。 也順帶示範——在「AI 已經會幫你讀設定、寫指令」的年代,為什麼這種誤判依然會發生。
先說清楚:Security Group 是什麼
Security Group(安全群組,以下簡稱 SG)是 AWS 的虛擬防火牆,掛在資源的網路介面 ENI(Elastic Network Interface,彈性網路介面)上。它有三個關鍵性質:
- 預設全部拒絕:沒有明確放行的流量一律擋掉。你只能「加白名單」,沒有「黑名單」這種東西。
- 來源可以是 IP 網段,也可以是另一個 SG:後者意思是「凡是掛了某個 SG 的資源一律放行」,不必管它的 IP 是多少——這在 VPC(Virtual Private Cloud,虛擬私有雲)內部互連時非常好用。
- stateful(狀態感知 — 放行了進來的連線,回程流量自動允許):不必另外開一條出站規則。
SG 的逐步設定我在 如何使用 psql 連線 AWS RDS 那篇有完整圖解(「SG 就像 RDS 的防火牆」)。這裡只談它跟「公開存取」旗標之間,那個最容易被誤會的互動。
旗標的陷阱:PubliclyAccessible=true 只給門牌,不給鑰匙
關鍵誤解就在這裡。PubliclyAccessible 這個旗標只決定一件事:RDS 要不要拿到一個公網 IP,以及一個可從網際網路解析的 DNS(網域名稱)。它完全不決定「誰連得進來」。
誰連得進來,是 SG 說了算。
所以 Public=true + SG 只放行 VPC 內網,實際結果是:這台 DB 有對外門牌(公網 IP),但大門被鎖死——任何外部 IP 走到門口就被擋下。
怎麼自己查:把「我覺得」變成「我看到」
與其腦補,不如打開 SG 的 inbound(入站)規則看一眼。AWS CLI 兩行就夠:
Step 1:找出 RDS 綁了哪些 SG
aws ec2 describe-db-instances --db-instance-identifier mydb \
--query 'DBInstances[0].VpcSecurityGroups[].VpcSecurityGroupId'
# → ["sg-0a1b2c3d4e5f6a7b8"]
Step 2:把每個 SG 的 inbound 規則攤開
aws ec2 describe-security-groups --group-ids sg-0a1b2c3d4e5f6a7b8 \
--query 'SecurityGroups[0].IpPermissions'
看的時候只問一個問題:inbound 來源裡,有沒有 0.0.0.0/0、或任何不是你預期的公網網段? 我那台查出來的 5432(PostgreSQL 連接埠)規則長這樣:
192.168.0.0/16 # 只放 VPC 內網段
sg-0a1b2c3d... (EKS 工作負載) # 加上掛了該 SG 的 pod
# ── 沒有任何公網 IP,更沒有 0.0.0.0/0
到這裡答案其實已經很清楚:門牌是公開的,門鎖只認內網。但「讀設定」跟「實際連得上嗎」之間,還差一個真正的測試。
實測打臉:同一台筆電,兩種結果
我從同一台筆電、同一個公網 IP(198.51.100.50),分別敲 RDS 和 EKS(Elastic Kubernetes Service,AWS 託管的 Kubernetes)的 API——前者用 nc(netcat — 測試 TCP 連接埠通不通的小工具),後者用 kubectl(Kubernetes 官方命令列工具):
# 1) 直接連 RDS 公網 endpoint 的 5432
$ nc -vz mydb.abc123xyz.ap-northeast-1.rds.amazonaws.com 5432
mydb...rds.amazonaws.com [203.0.113.71] 5432 (postgresql): Operation timed out # ❌ 不通
# 2) 用 kubectl 連 EKS(背後同樣是一個公網 endpoint)
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
ip-192-168-x-x.compute.internal Ready <none> 169d v1.32.9-eks # ✅ 成功
| 比較項 | RDS(PostgreSQL) | EKS API(kubectl) |
|---|---|---|
| 對外有公網位址? | 有 | 有 |
| 從我的筆電連 | ❌ timeout | ✅ 成功 |
兩個都掛在公網,為什麼一個通、一個不通?
答案:兩種把關層級——網路層 vs 身分層
差別不在「有沒有對外」,而在**「靠什麼把關」**:
| 比較項 | RDS | EKS API |
|---|---|---|
| 把關層級 | 網路層(L3/L4,OSI 的網路/傳輸層) | 身分層(L7,OSI 的應用層) |
| 機制 | Security Group 白名單 | IAM 身分 + Kubernetes RBAC |
| 比喻 | 門牌存在,但門鎖只認內網來的人,外人連門都敲不到 | 門對所有人開,但進門要刷識別證 |
| 沒有合法身分時 | 連 TCP 握手都不會成功(封包直接被丟棄) | TCP 連得上,但 API 回 401/403 |
EKS 的 API endpoint 設定其實是 endpointPublicAccess: true 且 publicAccessCidrs: ["0.0.0.0/0"]——對全世界開放。它敢這樣開,是因為真正的門鎖在身分層:沒有有效的 IAM(Identity and Access Management,身分與存取管理)憑證,再加上 Kubernetes 的 RBAC(Role-Based Access Control,基於角色的存取控制),連得到 endpoint 也辦不了任何事。
而 RDS 的 PostgreSQL 通訊協定,沒有這種「先驗 AWS 身分、再決定放不放行」的網路層機制,它只能靠 SG 在封包進門之前就擋掉。
timeout 還是 refused?這個差別會告訴你把關在哪一層
為什麼 nc 的結果是 timeout,而不是「連上後被拒絕」?因為 SG 在網路層直接把封包丟棄,你的 TCP SYN(建立連線的第一個握手封包)連一個回應都收不到,只能一路等到逾時。而 kubectl 是 TCP 先通、再由 IAM/RBAC 在應用層決定放不放行,所以會明確回你 401/403。
到底是 timeout 還是 refused/unauthorized,本身就在告訴你把關發生在哪一層。 這是排查連線問題時很實用的第一個分岔點。
flowchart LR
Laptop["我的筆電<br/>198.51.100.50"]
subgraph AWS["AWS VPC"]
EKS["EKS API endpoint<br/>公網, 0.0.0.0/0"]
RDS["RDS PostgreSQL<br/>公網 IP 203.0.113.71"]
IAM{"IAM + RBAC<br/>身分驗證"}
SG{"Security Group<br/>只放 VPC 內網"}
end
Laptop -->|"kubectl:TCP 先通"| EKS --> IAM -->|"憑證有效"| OK1["允許操作 ✅"]
Laptop -->|"nc 5432"| SG -->|"非內網來源"| Drop["封包丟棄<br/>→ timeout ❌"]
classDef ok fill:#d4edda,stroke:#28a745,color:#155724
classDef bad fill:#f8d7da,stroke:#dc3545,color:#721c24
classDef gate fill:#fff3cd,stroke:#856404,color:#856404
class OK1 ok
class Drop bad
class IAM,SG gate
真正該擔心的,反而是那個你以為安全的 endpoint
這趟盤點有個反轉:真正對 0.0.0.0/0 全開的不是一開始被 AI 點名的 RDS,而是 EKS 的 API endpoint。它沒出事,純粹是因為背後有 IAM + RBAC 雙重把關。但「靠身分層擋住」不代表「網路層就不必收斂」——多一道網路白名單,等於在憑證外洩時還有一層緩衝。
如果要收斂,方向有兩個:
| 做法 | 怎麼做 | 代價 |
|---|---|---|
| 限制來源 CIDR | 把 publicAccessCidrs 從 0.0.0.0/0 改成已知辦公室/VPN 出口 IP(CIDR 是 IP 網段寫法,如 203.0.113.0/24) | 浮動 IP 一變動就連不上,維護成本高 |
| 改走私有 endpoint | 關掉 public access、開 private access,本機透過 VPN 進 VPC | 要先有 VPN/跳板,設定較重 |
選擇邏輯其實跟前面同一條:先問「這個對外資源靠哪一層把關」,再決定要補網路層白名單、還是強化身分層。
flowchart TD
Start["看到一個有公網位址的資源"] --> Q{"它靠哪一層把關?"}
Q -->|"網路層 (SG)"| Net["檢查 SG inbound:<br/>有沒有 0.0.0.0/0 或非預期公網段"]
Q -->|"身分層 (IAM/RBAC)"| Id["確認憑證機制夠強,<br/>再評估是否收斂來源 CIDR"]
Net --> NetOk["白名單只有內網/已知來源<br/>→ 安全"]
Net --> NetBad["有 0.0.0.0/0<br/>→ 真的曝露,要收"]
Id --> IdAdv["可選:限制 CIDR<br/>或改 private + VPN"]
classDef start fill:#cce5ff,stroke:#007bff,color:#004085
classDef good fill:#d4edda,stroke:#28a745,color:#155724
classDef bad fill:#f8d7da,stroke:#dc3545,color:#721c24
classDef gate fill:#fff3cd,stroke:#856404,color:#856404
class Start start
class Q gate
class NetOk,IdAdv good
class NetBad bad
AI 時代的反思:為什麼一個旗標就能騙過 AI?
回到開頭——一個會讀設定、會寫 code 的 AI,為什麼看到 Public=true 就脫口而出「有資安風險」?
因為生成式判斷最擅長的,就是從局部訊號補出一套聽起來完整、自信的結論。Public=true →「公開」→「資安風險」,這條推理鏈在統計上太常見、太順了,順到不需要查證就能講得頭頭是道。但它跳過了一個關鍵事實:曝露與否的決定權不在這個旗標,而在 SG。
這正是「AI 都會寫 code 了,為什麼還會出事」的答案:AI 縮短的是「把訊號變成一句判斷」的距離,沒縮短「確認那句判斷為真」的距離。 局部資訊餵給模型,它能很快給你一個答案;但那個答案是「看起來對」還是「真的對」,只有能被推翻(falsifiable,可證偽)的驗證能分辨——以這篇來說,就是實際跑一次 nc 連連看。
而且驗證方式本身也有講究:「從真實的 IP,敲那個真實的 port」會明確回你 timeout 或 success,這才能證實或推翻那個判斷;但若改成截一張 VPC 架構圖來「確認安全」,那只是確認式驗證,根本看不出問題。能 falsify 的才算驗證,否則只是幫自己的直覺背書。
收斂:旗標是線索,連線測試才是真相
| 要判斷的事 | 不可靠的依據 | 可靠的做法 |
|---|---|---|
| RDS 是否真的對外曝露 | 看 PubliclyAccessible 旗標 | 讀 SG inbound 有沒有 0.0.0.0/0 或非預期公網網段 |
| 某個 port 通不通 | 憑設定腦補 | 從外部實際連,看 timeout(網路層擋)還是 refused/unauthorized(身分層擋) |
| 公開 endpoint 安不安全 | 「有公網就危險」 | 看它靠什麼把關——網路白名單,還是身分憑證 |
最後一個務實建議:即使 SG 已經鎖好,不需要公開的 RDS 仍建議設成 Public=false。這是 defense-in-depth(縱深防禦 — 多疊一層保險,不押注在單一機制上)——就算哪天有人手滑在 SG 加了一條 0.0.0.0/0,因為根本沒有公網路徑,也不會立刻被打穿。
下次再看到一個聽起來很可怕的旗標,先別急著下結論——敲一下那個 port,讓事實說話。
