Files
awoooi/apps/api/tests/test_config_url_validation.py
Your Name b1ef05fa8c
Some checks failed
Code Review / ai-code-review (push) Successful in 50s
CD Pipeline / tests (push) Failing after 1m14s
CD Pipeline / build-and-deploy (push) Has been skipped
CD Pipeline / post-deploy-checks (push) Has been skipped
feat(ollama): ADR-110 GCP 三層容災架構(GCP-A → GCP-B → Local → Gemini)
## 變更摘要
- Primary: http://34.143.170.20:11434 (GCP-A SSD, 9x 載速 + 2x 推理)
- Secondary: http://34.21.145.224:11434 (GCP-B SSD)
- Fallback: http://192.168.0.111:11434 (M1 Pro Local HDD,最後防線)
- 廢止 ADR-105「111 唯一鐵律」,新建 ADR-110

## 核心改動
- config.py: 新增 OLLAMA_SECONDARY_URL;validator 加 GCP IP 白名單(34.143.170.20, 34.21.145.224)
- ollama_failover_manager.py: 三層 Ollama 決策矩陣;並行健康檢查三台;health_111 → health_gcp_a
- ollama_health_monitor.py: host label 萃取改為通用版(支援 GCP 公網 IP)
- failover_alerter.py: 故障/恢復主機動態顯示,不再硬編碼「Ollama 111 (GPU)」
- ollama_auto_recovery.py: notify_recovery 改為 ollama_gcp_a;recovered_host 動態
- k8s/awoooi-prod: configmap + deployment + network-policy 同步更新(egress 加 GCP /32)
- 服務層: 10 個服務檔案硬編碼 192.168.0.111 改為讀 settings.OLLAMA_URL
- 測試: URL 常數更新,新增三層容災場景,GCP IP 白名單驗證測試

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 22:49:23 +08:00

167 lines
6.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
OLLAMA URL endpoint poisoning 防護測試 — Wave8-X2 + ADR-110
vuln #1OLLAMA_URL / OLLAMA_FALLBACK_URL 缺 IP allowlist 校驗
修法pydantic field_validator 拒絕非 private/loopback/known-hostname
2026-04-27 Wave8-X2 by Claude — vuln #1 + B14 + alerter memory dedup
2026-05-03 ogt: ADR-110 GCP 三層容災,更新 fixture 預設值為 GCP-A新增 GCP 白名單測試
"""
from __future__ import annotations
import pytest
from pydantic import ValidationError
# =============================================================================
# Helpers
# =============================================================================
def _make_settings(**kwargs):
"""建立 Settings 實例,只覆蓋指定欄位,其餘用安全預設值。"""
from src.core.config import Settings
base = {
"DATABASE_URL": "postgresql://u:p@localhost:5432/test",
"OLLAMA_URL": "http://34.143.170.20:11434", # GCP-A PrimaryADR-110 2026-05-03
"OLLAMA_FALLBACK_URL": "",
}
base.update(kwargs)
return Settings(**base)
# =============================================================================
# #1: 公網 IP 應被拒絕
# =============================================================================
def test_public_ip_rejected_in_ollama_url():
"""8.8.8.8 是公網 IP應被 validator 拒絕(端點中毒攻擊情境)"""
with pytest.raises(ValidationError, match="公網"):
_make_settings(OLLAMA_URL="http://8.8.8.8:11434")
def test_public_ip_rejected_in_ollama_fallback_url():
"""FALLBACK_URL 也受同一 validator 保護"""
with pytest.raises(ValidationError, match="公網"):
_make_settings(OLLAMA_FALLBACK_URL="http://1.1.1.1:11434")
# =============================================================================
# #2: 外部域名應被拒絕
# =============================================================================
def test_external_domain_rejected():
"""attacker.com 是外部域名(非 IP非白名單應被拒絕"""
with pytest.raises(ValidationError, match="外部域名"):
_make_settings(OLLAMA_URL="http://attacker.com:11434")
def test_external_domain_fallback_rejected():
"""FALLBACK_URL 外部域名也應被拒絕"""
with pytest.raises(ValidationError, match="外部域名"):
_make_settings(OLLAMA_FALLBACK_URL="http://evil.example.com:11434")
# =============================================================================
# #3: GCP 白名單公網 IP 應通過ADR-110 2026-05-03
# =============================================================================
def test_gcp_a_public_ip_accepted():
"""34.143.170.20 是 GCP-A 核准公網 IPADR-110 白名單),應通過"""
s = _make_settings(OLLAMA_URL="http://34.143.170.20:11434")
assert s.OLLAMA_URL == "http://34.143.170.20:11434"
def test_gcp_b_public_ip_accepted():
"""34.21.145.224 是 GCP-B 核准公網 IPADR-110 白名單),應通過"""
s = _make_settings(OLLAMA_URL="http://34.21.145.224:11434")
assert s.OLLAMA_URL == "http://34.21.145.224:11434"
def test_gcp_b_as_secondary_url_accepted():
"""GCP-B 作為 OLLAMA_SECONDARY_URL 也應通過白名單"""
s = _make_settings(
OLLAMA_URL="http://34.143.170.20:11434",
OLLAMA_SECONDARY_URL="http://34.21.145.224:11434",
)
assert s.OLLAMA_SECONDARY_URL == "http://34.21.145.224:11434"
def test_arbitrary_public_ip_rejected():
"""8.8.8.8 非 GCP 白名單公網 IP仍應被拒絕端點中毒攻擊防護"""
with pytest.raises(ValidationError, match="公網"):
_make_settings(OLLAMA_URL="http://8.8.8.8:11434")
# =============================================================================
# #4: 私網 IP 應通過
# =============================================================================
def test_private_ip_192_168_accepted():
"""192.168.0.111 是 RFC1918 私網 IPLocal HDD 後備主機),應通過"""
s = _make_settings(OLLAMA_URL="http://192.168.0.111:11434")
assert s.OLLAMA_URL == "http://192.168.0.111:11434"
def test_private_ip_10_x_accepted():
"""10.x.x.x 是 RFC1918 私網 IP應通過"""
s = _make_settings(OLLAMA_URL="http://10.0.0.5:11434")
assert s.OLLAMA_URL == "http://10.0.0.5:11434"
def test_private_ip_172_16_accepted():
"""172.16.x.x 是 RFC1918 私網 IP應通過"""
s = _make_settings(OLLAMA_FALLBACK_URL="http://172.16.0.10:11434")
assert s.OLLAMA_FALLBACK_URL == "http://172.16.0.10:11434"
# =============================================================================
# #5: localhost / loopback 應通過
# =============================================================================
def test_localhost_accepted():
"""localhost 在 known hostname 白名單,應通過"""
s = _make_settings(OLLAMA_URL="http://localhost:11434")
assert s.OLLAMA_URL == "http://localhost:11434"
def test_loopback_ip_accepted():
"""127.0.0.1 是 loopback IP應通過"""
s = _make_settings(OLLAMA_URL="http://127.0.0.1:11434")
assert s.OLLAMA_URL == "http://127.0.0.1:11434"
# =============================================================================
# #6: 已知 K8s Service hostname 應通過
# =============================================================================
def test_known_k8s_svc_ollama_svc_accepted():
"""ollama-svc 在 K8s Service 白名單,應通過"""
s = _make_settings(OLLAMA_URL="http://ollama-svc:11434")
assert s.OLLAMA_URL == "http://ollama-svc:11434"
def test_known_k8s_svc_ollama_fallback_svc_accepted():
"""ollama-fallback-svc 在白名單,應通過"""
s = _make_settings(OLLAMA_FALLBACK_URL="http://ollama-fallback-svc:11434")
assert s.OLLAMA_FALLBACK_URL == "http://ollama-fallback-svc:11434"
# =============================================================================
# #7: 空字串應通過OLLAMA_FALLBACK_URL 預設值)
# =============================================================================
def test_empty_string_fallback_url_accepted():
"""OLLAMA_FALLBACK_URL 預設空字串(未設定),應通過"""
s = _make_settings(OLLAMA_FALLBACK_URL="")
assert s.OLLAMA_FALLBACK_URL == ""