fix(awooop): Phase 2 第二批 P0 安全強化 + Redis key 命名空間修正
## P0-05 Callback Nonce 防偽造(ADR-116)
- security_interceptor.py:generate_callback_nonce() 新增 HMAC-SHA256[:16] 附加
- 新 5-part 格式:{action}:{short_id}:{ts}:{rand}:{hmac16}
- CALLBACK_HMAC_SECRET 未設定時降級 warning(向後相容)
- security_interceptor.py:parse_callback_data() 新增 5-part 分支 + HMAC 驗證
- config.py:新增 CALLBACK_HMAC_SECRET: str = Field(default="")
## P0-06 Webhook HMAC Replay 防護(ADR-116)
- security_interceptor.py:新增 check_webhook_nonce()(Service 層,get_redis 在此層合法)
- webhooks.py:verify_webhook_signature() 新增兩個可選 Header
- X-Webhook-Timestamp:±300s 窗口驗證(若提供)
- X-Webhook-Nonce:呼叫 check_webhook_nonce()(Redis NX dedup,fail open)
- 移除直接 get_redis import(leWOOOgo 積木化修正)
## P0-11 ollama:current_primary Redis key 遷移 Phase A(ADR-110)
- ollama_auto_recovery.py:_REDIS_PRIMARY_KEY = "platform:ollama:current_primary"
- 雙寫舊 key "ollama:current_primary"(Phase A 30 天)
- 讀取以新 key 為主,fallback 舊 key
## P0-12 consensus Redis key 加 project namespace Phase A
- consensus_engine.py:新增 _consensus_key() / _consensus_legacy_key() helper
- 新 key:{project_id}:consensus:{consensus_id}
- project_id=None 時 fallback __platform__:consensus:{consensus_id}
- Phase A 雙寫 + fallback 讀取,現有呼叫方零修改
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,7 @@ from src.core.config import settings
|
||||
from src.core.constants import is_cicd_alertname, is_heartbeat_alertname
|
||||
from src.services.alert_rule_engine import get_incident_type, match_rule
|
||||
from src.services.action_parser import is_safe_kubectl_action
|
||||
from src.services.security_interceptor import check_webhook_nonce # P0-06: nonce dedup via Service 層
|
||||
from src.core.logging import get_logger
|
||||
from src.core.metrics import record_alert_chain_success
|
||||
|
||||
@@ -648,6 +649,8 @@ class HMACVerificationError(Exception):
|
||||
async def verify_webhook_signature(
|
||||
request: Request,
|
||||
x_signature_256: str | None = Header(None, alias="X-Signature-256"),
|
||||
x_webhook_timestamp: str | None = Header(None, alias="X-Webhook-Timestamp"),
|
||||
x_webhook_nonce: str | None = Header(None, alias="X-Webhook-Nonce"),
|
||||
) -> bool:
|
||||
"""
|
||||
驗證 Webhook 請求的 HMAC-SHA256 簽章
|
||||
@@ -657,6 +660,11 @@ async def verify_webhook_signature(
|
||||
- 簽章格式: sha256=<hex_digest>
|
||||
- 使用 WEBHOOK_HMAC_SECRET 進行驗證
|
||||
|
||||
ADR-116 Replay 防護(向後相容):
|
||||
- X-Webhook-Timestamp: Unix epoch 秒,若提供則驗證 ±300 秒範圍
|
||||
- X-Webhook-Nonce: 隨機字串,若提供則用 Redis NX 去重(TTL=600s)
|
||||
- 兩個 Header 均可選;過渡期不提供時僅記錄 warning
|
||||
|
||||
安全鐵律 (Fail-Closed):
|
||||
- 生產環境: HMAC Secret 未設定 → 直接拒絕 (不可跳過)
|
||||
- 開發環境: 可跳過驗證 (僅供本地測試)
|
||||
@@ -664,6 +672,8 @@ async def verify_webhook_signature(
|
||||
Args:
|
||||
request: FastAPI Request 物件
|
||||
x_signature_256: X-Signature-256 Header 值
|
||||
x_webhook_timestamp: X-Webhook-Timestamp Header 值(Unix epoch 秒,可選)
|
||||
x_webhook_nonce: X-Webhook-Nonce Header 值(隨機字串,可選)
|
||||
|
||||
Returns:
|
||||
bool: 驗證是否通過
|
||||
@@ -671,6 +681,8 @@ async def verify_webhook_signature(
|
||||
Raises:
|
||||
HMACVerificationError: 簽章驗證失敗
|
||||
"""
|
||||
import time as _time
|
||||
|
||||
# ==========================================================================
|
||||
# Fail-Closed 安全策略 (CISO 要求)
|
||||
# ==========================================================================
|
||||
@@ -725,6 +737,54 @@ async def verify_webhook_signature(
|
||||
raise HMACVerificationError("Invalid signature")
|
||||
|
||||
logger.info("hmac_verification_success")
|
||||
|
||||
# ==========================================================================
|
||||
# ADR-116: Replay 防護(向後相容,HMAC 驗證成功後才執行)
|
||||
# ==========================================================================
|
||||
|
||||
# --- Timestamp 驗證(±300 秒) ---
|
||||
if x_webhook_timestamp is not None:
|
||||
try:
|
||||
req_ts = int(x_webhook_timestamp)
|
||||
now_ts = int(_time.time())
|
||||
skew = abs(now_ts - req_ts)
|
||||
if skew > 300:
|
||||
logger.warning(
|
||||
"webhook_timestamp_out_of_window",
|
||||
request_ts=req_ts,
|
||||
server_ts=now_ts,
|
||||
skew_seconds=skew,
|
||||
)
|
||||
raise HMACVerificationError(
|
||||
f"Timestamp out of acceptable window (skew={skew}s > 300s)"
|
||||
)
|
||||
except ValueError:
|
||||
logger.warning(
|
||||
"webhook_timestamp_invalid_format",
|
||||
raw_value=x_webhook_timestamp,
|
||||
)
|
||||
raise HMACVerificationError("X-Webhook-Timestamp must be a Unix epoch integer")
|
||||
else:
|
||||
# 過渡期:沒有提供 Timestamp 則記錄 warning 但允許通過
|
||||
logger.warning(
|
||||
"webhook_replay_protection_missing",
|
||||
header="X-Webhook-Timestamp",
|
||||
note="transition period: request allowed but sender should add replay headers",
|
||||
)
|
||||
|
||||
# --- Nonce 去重(透過 security_interceptor.check_webhook_nonce,fail open) ---
|
||||
if x_webhook_nonce is not None:
|
||||
valid = await check_webhook_nonce(x_webhook_nonce)
|
||||
if not valid:
|
||||
raise HMACVerificationError("Nonce replay detected")
|
||||
else:
|
||||
# 過渡期:沒有提供 Nonce 則記錄 warning 但允許通過
|
||||
logger.warning(
|
||||
"webhook_replay_protection_missing",
|
||||
header="X-Webhook-Nonce",
|
||||
note="transition period: request allowed but sender should add replay headers",
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user