fix(sre): reject retired host110 ollama routes

This commit is contained in:
Your Name
2026-07-19 03:03:44 +08:00
parent 092393f28f
commit 3223f9c913
4 changed files with 53 additions and 13 deletions

View File

@@ -374,6 +374,27 @@ class Settings(BaseSettings):
if not host:
raise ValueError(f"OLLAMA URL 缺少 hostname{v!r}")
try:
parsed_ip = ipaddress.ip_address(host)
except ValueError:
parsed_ip = None
canonical_ip = (
parsed_ip.ipv4_mapped
if isinstance(parsed_ip, ipaddress.IPv6Address)
and parsed_ip.ipv4_mapped is not None
else parsed_ip
)
# Host110 的 Ollama transport/proxy 已退役。私網位址本身符合 SSRF
# allowlist 並不代表仍是合法資產;若舊 ConfigMap/env 漂移回 Host110
# 必須在任何 health probe、fallback 或 Telegram 告警前 fail closed。
# Canonical IP comparison also closes IPv4-mapped IPv6 aliases.
if canonical_ip == ipaddress.ip_address("192.168.0.110"):
raise ValueError(
"asset_identity_unresolved: Host110 Ollama transport is retired; "
f"received {v!r}"
)
# Kubernetes Service hostname 白名單K8s DNS + 開發別名)
_ALLOWED_HOSTNAMES = {
"localhost",
@@ -397,14 +418,13 @@ class Settings(BaseSettings):
return v
# 否則必須是 private/loopback IP
try:
ip = ipaddress.ip_address(host)
except ValueError:
if parsed_ip is None:
# hostname 不是 IP 也不在白名單 → 拒絕
raise ValueError(
f"OLLAMA URL host 不允許的外部域名:{host!r}(完整 URL{v!r}"
",必須使用私網 IP 或已知 K8s Service hostname"
) from None
ip = parsed_ip
if not (ip.is_private or ip.is_loopback):
raise ValueError(
f"OLLAMA URL 必須是私網/loopback IP、已知 K8s SVC 或 GCP 白名單 IP"

View File

@@ -108,6 +108,21 @@ def test_private_ip_192_168_accepted():
assert s.OLLAMA_URL == "http://192.168.0.111:11434"
@pytest.mark.parametrize(
("field", "url"),
(
("OLLAMA_URL", "http://192.168.0.110:11435"),
("OLLAMA_SECONDARY_URL", "http://192.168.0.110:11436"),
("OLLAMA_FALLBACK_URL", "http://192.168.0.110:11437"),
("OLLAMA_URL", "http://[::ffff:192.168.0.110]:11435"),
),
)
def test_retired_host110_ollama_transport_is_rejected(field: str, url: str):
"""Retired Host110 must never re-enter any provider slot via stale env/config."""
with pytest.raises(ValidationError, match="asset_identity_unresolved"):
_make_settings(**{field: url})
def test_private_ip_10_x_accepted():
"""10.x.x.x 是 RFC1918 私網 IP應通過"""
s = _make_settings(OLLAMA_URL="http://10.0.0.5:11434")

View File

@@ -10,9 +10,9 @@ from src.services.ollama_endpoint_resolver import (
def _settings(
*,
primary: str = "http://192.168.0.110:11435",
secondary: str = "http://192.168.0.110:11436",
fallback: str = "http://192.168.0.110:11437",
primary: str = "http://34.143.170.20:11434",
secondary: str = "http://34.21.145.224:11434",
fallback: str = "http://192.168.0.111:11434",
) -> SimpleNamespace:
return SimpleNamespace(
OLLAMA_URL=primary,
@@ -42,7 +42,7 @@ def test_all_workloads_prefer_gcp_a_lane() -> None:
"dr",
):
selection = resolve_ollama_selection(workload, config=cfg)
assert selection.url == "http://192.168.0.110:11435"
assert selection.url == "http://34.143.170.20:11434"
assert selection.provider_name == "ollama_gcp_a"
assert selection.reason == "global_primary_gcp_a"
@@ -53,9 +53,9 @@ def test_general_workloads_share_global_ollama_order() -> None:
for workload in ("interactive", "deep_rca", "local_required", "privacy_sensitive", "dr"):
order = resolve_ollama_order(workload, config=cfg)
assert [selection.url for selection in order] == [
"http://192.168.0.110:11435",
"http://192.168.0.110:11436",
"http://192.168.0.110:11437",
"http://34.143.170.20:11434",
"http://34.21.145.224:11434",
"http://192.168.0.111:11434",
]
assert [selection.provider_name for selection in order] == [
"ollama_gcp_a",
@@ -78,14 +78,14 @@ def test_alert_fast_uses_gcp_b_when_gcp_a_is_not_configured() -> None:
selection = resolve_ollama_selection("alert_fast", config=_settings(primary=""))
assert selection.provider_name == "ollama_gcp_b"
assert selection.url == "http://192.168.0.110:11436"
assert selection.url == "http://34.21.145.224:11434"
def test_non_sensitive_workloads_fall_back_to_gcp_b_when_primary_missing() -> None:
cfg = _settings(primary="")
selection = resolve_ollama_selection("embedding", config=cfg)
assert selection.url == "http://192.168.0.110:11436"
assert selection.url == "http://34.21.145.224:11434"
assert selection.provider_name == "ollama_gcp_b"
assert selection.reason == "global_secondary_gcp_b"

View File

@@ -335,7 +335,10 @@
"docs/operations/portfolio-infrastructure-asset-reconciliation.snapshot.json",
"docs/operations/portfolio-infrastructure-asset-reconciliation-handoff.md",
"apps/api/src/services/portfolio_infrastructure_asset_reconciliation.py",
"apps/api/src/core/config.py",
"apps/api/src/api/v1/agents.py",
"apps/api/tests/test_config_url_validation.py",
"apps/api/tests/test_ollama_endpoint_resolver.py",
"apps/api/tests/test_portfolio_infrastructure_asset_reconciliation_api.py",
"scripts/ops/retire-host110-ollama-proxy.sh",
"scripts/ops/render-service-registry-configmap.py",
@@ -348,16 +351,18 @@
"confirmed_truth": [
"Host110 Ollama has been removed and must remain a retired tombstone, never a fallback endpoint",
"AIA-SRE-002 run-aia-sre-002-20260716-1752 removed the exact stale Nginx 11435 proxy with rollback backup and independently verified listeners 11435/11434=0, Ollama containers=0, stale config paths=0, nginx active/test pass and Prometheus host110 Ollama targets=0",
"The Settings asset gate rejects native and IPv4-mapped IPv6 Host110 Ollama URLs as asset_identity_unresolved before any health probe, fallback decision or Telegram alert; focused URL/order/manifest tests pass locally",
"Host111 is the third provider hop and runs as a macOS LaunchAgent, not systemd",
"GCP-A and GCP-B source targets are public HTTP sanitized candidates only",
"gitea-native source target is not production runtime evidence while the legacy GitHub exporter remains visible"
],
"runtime_gaps": [
"The Host110 stale-endpoint asset gate has source and local-test evidence only; production startup/readback has not yet proven the retired endpoint cannot recur",
"Host111 LaunchAgent lacks completed local plus host120/host121 origin readback and a fresh exact sensor series",
"GCP-A/GCP-B exact Prometheus target series are missing in production",
"gitea-native exact target is missing and legacy GitHub exporter runtime drift is unresolved"
],
"next_action": "deploy the reconciled tombstone/targets, preserve the verified Host110 absence receipt, then verify Host111 LaunchAgent from local and host120/121, GCP-A/GCP-B exact Prometheus series, gitea-native target freshness and legacy exporter absence under one source SHA"
"next_action": "deploy the Host110 asset gate with the reconciled tombstone/targets, preserve the verified Host110 absence receipt, then verify Host111 LaunchAgent from local and host120/121, GCP-A/GCP-B exact Prometheus series, gitea-native target freshness and legacy exporter absence under one source SHA"
},
{
"id": "AIA-SRE-003",