Files
awoooi/docs/adr/ADR-116-awooop-security-hardening.md
Your Name 13e51802fe feat(awooop): Phase 0 全 ADR + Phase 1 control plane schema(含 critic 四項修正)
## 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>
2026-05-04 13:37:11 +08:00

7.7 KiB
Raw Permalink Blame History

ADR-116: AwoooP Security Hardening

狀態Accepted 日期2026-05-03台北 決策者:統帥 範圍callback nonce 修補、webhook replay 防護、approval_token 規格、requires_approval 修補 關聯ADR-112contract governance、ADR-114approval flow


背景

vuln-verifier 對 codebase 進行 PoC 驗證,確認三個安全漏洞真實存在:

漏洞 1Callback Nonce 偽造P0-05

  • 位置:apps/api/src/services/security_interceptor.py:451-490
  • 問題server nonce 的驗證邏輯不依賴 server_secret,攻擊者可在不知道 secret 的情況下構造通過驗證的 nonce
  • PoC已確認可通過驗證

漏洞 2Webhook HMAC Replay 無防護P0-06

  • 位置:apps/api/src/api/v1/webhooks.py:679-728
  • 問題webhook HMAC 驗證只驗 body hash沒有 timestamp 或 nonce
  • PoC截取合法 webhook request重放到任意時間仍通過驗證

漏洞 3requires_approval 由 LLM Output 決定P0-04

  • 位置:apps/api/src/services/decision_manager.pyapproval 鏈)
  • 問題: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 必須帶:

  1. X-Timestamp headerUnix timestamp
  2. X-Nonce header16-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 NXTTL 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 TLSmTLS代替不需要 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 → 必須 approval
  • action_type=knowledge_write → 不需 approval低風險
  • action_type=destructive_mcp_tool → 必須 approval不論 risk_level
  • 以上規則寫在 policy contract不在 LLM prompt 中

D4 — approval_token 規格Phase 8

算法HS256
簽章 KeyK8s Secret `awooop-approval-signing-key`

Payload:
  iss: "awooop-platform"
  sub: "{project_id}:{run_id}"
  aud: "awooop-approval"
  jti: UUID唯一 ID存 Redis NXTTL 15min
  exp: now + 90015 分鐘)
  iat: now
  approval_type: "human" | "system"
  decision_scope: [{tool_id}, ...]  # 這個 token 授權哪些 tool 的執行

驗證Phase 8 resume API
  1. HS256 簽章驗證
  2. exp 未過期
  3. Redis NXSET 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 HMACADR-112

實作優先序Phase 2

  1. PR-07a立即security_interceptor.py nonce 重設計
  2. PR-07b立即webhooks.py timestamp + nonce 加入
  3. PR-07c立即decision_manager.py requires_approval 改為 policy-derived
  4. Phase 4approval_tokenHS256 token 生成 + Phase 8 verify

後果

Benefits

  • 三個 PoC 確認的漏洞全部修補
  • approval 決策權回到 policy不再由 LLM 決定),防止 prompt injection 繞過

Costs

  • webhook caller 需要加 X-Timestamp + X-Nonce header需要 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 8approval_token 無 token resume 被拒絕E-APPROVAL-001

關聯

  • ADR-114approval flowWAITING_APPROVAL state
  • ADR-112contract activationapproval workflow
  • ADR-122OWASP Agentic AI Top 10 — OAI-03 Excessive Agency、OAI-09 Access Control Bypass