## Phase 0(文件層,全部 Accepted) - ADR-106/107:AwoooP 平台架構 + 儲存策略 - ADR-111~118:Bootstrap → RLS 七項核心 ADR - ADR-119~124:SAGA → Singleton Decomposition 六項 ADR - ADR-UI-01~04:Operator Console 四個 UI ADR ## Phase 1(DB schema + migration) - awooop_phase1_control_plane_2026-05-04.sql:7 張新表 + trigger + RLS - Step 1:三角色(platform_admin/migration BYPASSRLS,awooop_app 受 RLS) - Step 13:GRANT awooop_app 最小權限(7 條) - Step 14:RLS fail-closed,移除 __platform__ 後門 - awooop_phase1_batch1_rls_2026-05-04.sql:高流量四表三步式 ADD COLUMN - awooop_phase1_batch1_backfill.py:SKIP LOCKED 分批回填腳本 - awooop_models.py:7 個 SQLAlchemy 2.x models ## Critic 修正(4 Critical + 3 Major) - C-1:ADD CONSTRAINT IF NOT EXISTS → DO 塊 + pg_constraint 查詢 - C-2:__mapper_args__ 字串 list → primary_key=True on mapped_column - C-3:__platform__ RLS 後門 → 全移除,改用 BYPASSRLS role - C-4:awooop_app role 從未建立 → Step 1 + 7 條 GRANT - M-1:active_pointer_guard SECURITY DEFINER(FORCE RLS 跨租戶保護) - M-2:pg_partman create_parent 加冪等防護 - M-3:immutability trigger 新增身份欄位保護(project_id/family/contract_id) ## Task 1.2 修補 - agent_loader.py:硬編碼 Mac 路徑 → AGENTS_DIR 環境變數 - Dockerfile:補 COPY .claude/agents/ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
7.7 KiB
7.7 KiB
ADR-116: AwoooP Security Hardening
狀態:Accepted 日期:2026-05-03(台北) 決策者:統帥 範圍:callback nonce 修補、webhook replay 防護、approval_token 規格、requires_approval 修補 關聯:ADR-112(contract governance)、ADR-114(approval flow)
背景
vuln-verifier 對 codebase 進行 PoC 驗證,確認三個安全漏洞真實存在:
漏洞 1:Callback Nonce 偽造(P0-05)
- 位置:
apps/api/src/services/security_interceptor.py:451-490 - 問題:server nonce 的驗證邏輯不依賴
server_secret,攻擊者可在不知道 secret 的情況下構造通過驗證的 nonce - PoC:已確認可通過驗證
漏洞 2:Webhook HMAC Replay 無防護(P0-06)
- 位置:
apps/api/src/api/v1/webhooks.py:679-728 - 問題:webhook HMAC 驗證只驗 body hash,沒有 timestamp 或 nonce
- PoC:截取合法 webhook request,重放到任意時間,仍通過驗證
漏洞 3:requires_approval 由 LLM Output 決定(P0-04)
- 位置:
apps/api/src/services/decision_manager.py(approval 鏈) - 問題:
requires_approval欄位的值來自 LLM 的 response,攻擊者可透過 prompt injection 讓 LLM 輸出requires_approval=false,繞過 approval 要求 - PoC:已確認可繞過
決策
D1 — Callback Nonce 重設計
修補前(不安全):
# server 端生成 nonce:僅用時間戳或隨機值,不含 secret
server_nonce = hash(timestamp + random)
# 驗證時:只比對 client 提供的 nonce == server_nonce(不驗 secret 參與)
修補後(安全):
import hmac, hashlib, secrets
def generate_callback_nonce(run_id: str, server_secret: str) -> str:
"""生成含 secret 的 nonce,防止偽造"""
salt = secrets.token_hex(16)
message = f"{run_id}:{salt}:{int(time.time())}"
signature = hmac.new(
server_secret.encode(),
message.encode(),
hashlib.sha256
).hexdigest()
return f"{salt}:{signature}"
def verify_callback_nonce(nonce: str, run_id: str, server_secret: str,
max_age_seconds: int = 300) -> bool:
"""驗證 nonce,含 server_secret 參與,拒絕超過 5 分鐘的 nonce"""
try:
salt, provided_sig = nonce.split(":", 1)
# 重建 message(注意:時間驗證需要 timestamp 在 nonce 中)
# 完整實作需在 nonce 中包含 timestamp
...
except ValueError:
return False
# server_secret 從 K8s Secret 注入(settings.CALLBACK_NONCE_SECRET)
# 禁止 hardcode
D2 — Webhook Timestamp + Nonce
修補方案:
每個 webhook request 必須帶:
X-Timestampheader:Unix timestamp(秒)X-Nonceheader:16-byte random hex(防 replay)
驗證邏輯:
async def verify_webhook_security(request: Request, body: bytes) -> None:
timestamp = request.headers.get("X-Timestamp")
nonce = request.headers.get("X-Nonce")
if not timestamp or not nonce:
raise WebhookSecurityError("Missing security headers")
# 1. Timestamp 驗證(±5 分鐘 window)
ts = int(timestamp)
if abs(time.time() - ts) > 300:
raise WebhookSecurityError("Timestamp out of window")
# 2. Nonce 去重(Redis NX,TTL 10 分鐘)
nonce_key = f"platform:webhook:nonce:{nonce}"
if not await redis.set(nonce_key, "1", nx=True, ex=600):
raise WebhookSecurityError("Nonce already used (replay attack)")
# 3. HMAC 驗證(原有邏輯,保留)
# hmac_header = request.headers.get("X-Hub-Signature-256")
# verify_hmac(body, hmac_header, settings.WEBHOOK_SECRET)
對 Gitea / Alertmanager 的影響:
- Gitea webhook:支援 secret 但未必加 timestamp,需要在 Gitea 設定端更新 secret policy
- Alertmanager:不支援自訂 header,例外處理:Alertmanager 請求允許 timestamp 寬鬆 window(改為 ±10 分鐘,但仍要 nonce)
- 內部呼叫(K8s Job / internal services):使用 mutual TLS(mTLS)代替,不需要 timestamp/nonce
D3 — requires_approval 改為 Policy-Derived
修補前(不安全):
# LLM response 中有 requires_approval 欄位,被直接採用
decision = llm_response.get("requires_approval", False)
修補後(安全):
async def should_require_approval(
project_id: str,
agent_id: str,
action_type: str,
risk_level: str,
effective_policy: EffectivePolicy
) -> bool:
"""
approval 要求從 policy contract 決定,禁止由 LLM output 決定。
LLM 的 'requires_approval' 欄位被忽略。
"""
# 從 effective_policy 的 approval_gates 讀取
for gate in effective_policy.approval_gates:
if gate.matches(action_type=action_type, risk_level=risk_level):
return True
return False
規則:
action_type=auto_repair+risk_level >= HIGH→ 必須 approvalaction_type=knowledge_write→ 不需 approval(低風險)action_type=destructive_mcp_tool→ 必須 approval(不論 risk_level)- 以上規則寫在 policy contract,不在 LLM prompt 中
D4 — approval_token 規格(Phase 8)
算法:HS256
簽章 Key:K8s Secret `awooop-approval-signing-key`
Payload:
iss: "awooop-platform"
sub: "{project_id}:{run_id}"
aud: "awooop-approval"
jti: UUID(唯一 ID,存 Redis NX,TTL 15min)
exp: now + 900(15 分鐘)
iat: now
approval_type: "human" | "system"
decision_scope: [{tool_id}, ...] # 這個 token 授權哪些 tool 的執行
驗證(Phase 8 resume API):
1. HS256 簽章驗證
2. exp 未過期
3. Redis NX:SET awooop:approval:jti:{jti} "used" NX EX 900
→ 若 SET 失敗 → E-APPROVAL-003(已使用)
4. sub 的 run_id 與 API path 的 run_id 一致
5. decision_scope 包含 run 要求執行的 tool
D5 — Secret 管理
所有 security-critical secret 必須從 K8s Secret 注入,禁止硬碼:
| Secret 名稱 | K8s Secret Key | 用途 |
|---|---|---|
CALLBACK_NONCE_SECRET |
awooop-security/callback-nonce-secret |
callback nonce HMAC |
WEBHOOK_SECRET |
現有 awoooi-secrets/webhook-secret |
webhook HMAC(已有) |
APPROVAL_SIGNING_KEY |
awooop-security/approval-signing-key |
approval_token HS256 |
CONTRACT_SIGNING_KEY |
awooop-security/contract-signing-key |
contract publish HMAC(ADR-112) |
實作優先序(Phase 2)
- PR-07a(立即):security_interceptor.py nonce 重設計
- PR-07b(立即):webhooks.py timestamp + nonce 加入
- PR-07c(立即):decision_manager.py requires_approval 改為 policy-derived
- Phase 4(approval_token):HS256 token 生成 + Phase 8 verify
後果
Benefits
- 三個 PoC 確認的漏洞全部修補
- approval 決策權回到 policy(不再由 LLM 決定),防止 prompt injection 繞過
Costs
- webhook caller 需要加
X-Timestamp+X-Nonceheader(需要 Gitea/Alertmanager 設定調整) - callback nonce 需要額外 Redis 查詢(nonce 去重)
Risks
- K8s Secret rotation 時,正在驗證中的 nonce/token 可能失效
- 緩解:rotation 時短暫雙 key 驗證(新 key 生成後,舊 key 再維持 TTL 時間)
驗收標準
- vuln-verifier PoC 重跑:P0-05 nonce 偽造失敗
- vuln-verifier PoC 重跑:P0-06 webhook replay 失敗
- vuln-verifier PoC 重跑:P0-04 LLM 無法決定 requires_approval
- Phase 8:approval_token 無 token resume 被拒絕(E-APPROVAL-001)
關聯
- ADR-114(approval flow,WAITING_APPROVAL state)
- ADR-112(contract activation,approval workflow)
- ADR-122(OWASP Agentic AI Top 10 — OAI-03 Excessive Agency、OAI-09 Access Control Bypass)