docs(web): 鎖住公開前端 env 範例拓樸 [skip ci]

This commit is contained in:
Your Name
2026-06-15 13:16:41 +08:00
parent 7529232fd9
commit 952c92888b
4 changed files with 108 additions and 9 deletions

View File

@@ -9,7 +9,7 @@
# ----------------------------------------------------------------------------
# API 後端 URLNext.js build-time 寫入 JS bundle禁止使用內網 IP
NEXT_PUBLIC_API_URL=http://192.168.0.188:32334
NEXT_PUBLIC_API_URL=https://awoooi.wooo.work
# ----------------------------------------------------------------------------
# 可選 (OPTIONAL)
@@ -18,18 +18,15 @@ NEXT_PUBLIC_API_URL=http://192.168.0.188:32334
# 是否啟用 Demo 模式true/false
NEXT_PUBLIC_ENABLE_DEMO=false
# SignOz 可觀測性平台 URL
NEXT_PUBLIC_SIGNOZ_URL=http://192.168.0.110:3301
# SignOz 可觀測性平台 URL(公開前端只允許公網入口)
NEXT_PUBLIC_SIGNOZ_URL=https://signoz.wooo.work
# 主機 IP 列表逗號分隔live-dashboard 用於 fallback 顯示)
NEXT_PUBLIC_HOST_IPS=192.168.0.110,192.168.0.112,192.168.0.120,192.168.0.188
# K8s Cluster VIP 資訊字串host-grid 顯示用)
NEXT_PUBLIC_K8S_VIP_INFO=VIP 192.168.0.125 · kubectl :6443 · Web :32335 · API :32334
# 主機清單與 K8s VIP 顯示由後端提供遮罩後的資產代號。
# 不得在公開前端 env 寫入實際內網拓樸。
# ----------------------------------------------------------------------------
# Server-side Only不含 NEXT_PUBLIC_ 前綴,不會暴露在 JS bundle
# ----------------------------------------------------------------------------
# Sentry 自建主機 URLsentry-tunnel route handler 使用)
SENTRY_HOST=http://192.168.0.110:9000
SENTRY_HOST=https://sentry.wooo.work

View File

@@ -1,3 +1,34 @@
## 2026-06-15公開前端 env 範例內網拓樸防回歸
**背景**production `/api/sentry-tunnel` 已不再回顯實際 upstream target但 repo 內 `apps/web/.env.example` 仍保留多個 `NEXT_PUBLIC_*` 內網 IP 範例與 server-side `SENTRY_HOST` 內網 URL。這不一定代表目前正式站已外洩但會讓未來複製範例建置時把內網拓樸 bake 進 JS bundle 或公開文件,違反前端內網 IP 禁令。
**完成項目**
- `apps/web/.env.example` 改為只使用公網入口:`NEXT_PUBLIC_API_URL=https://awoooi.wooo.work``NEXT_PUBLIC_SIGNOZ_URL=https://signoz.wooo.work``SENTRY_HOST=https://sentry.wooo.work`
- 移除 active `NEXT_PUBLIC_HOST_IPS``NEXT_PUBLIC_K8S_VIP_INFO` 範例值,改以繁中註記說明主機清單與 K8s VIP 顯示需由後端提供遮罩後的資產代號。
- 新增 `scripts/security/public-frontend-env-guard.py`,檢查公開前端 env 範例不得包含 RFC1918 內網 IP、不得重新啟用已停用的公開拓樸 env key`NEXT_PUBLIC_*` 範例需有明確安全預設。
- `scripts/security/security-mirror-progress-guard.py` 已接入 public frontend env guard讓 IwoooS 主 guard 一併鎖住此類回歸。
**Production 只讀驗證**
- `https://awoooi.wooo.work/api/sentry-tunnel``200`body 為 `{"status":"ok","tunnel":"/api/sentry-tunnel","target_configured":true}`;沒有實際 upstream target、內網 IP、`SENTRY_HOST``NEXT_PUBLIC_SENTRY_DSN`
- `https://awoooi.wooo.work/api/v1/health``200`;未命中內網 IP、`SENTRY_HOST``NEXT_PUBLIC_SENTRY_DSN`、工作視窗字串、個人 owner namespace、raw blocker 狀態。
- `https://awoooi.wooo.work/zh-TW/iwooos?_v=7529232f-public-scan``https://awoooi.wooo.work/zh-TW/awooop/tenants?_v=7529232f-public-scan``200`;未命中內網 IP、工作視窗字串、個人 owner namespace、raw blocker 狀態。
**本地驗證**
- `python3 -m py_compile scripts/security/public-frontend-env-guard.py scripts/security/security-mirror-progress-guard.py` 通過。
- `python3 scripts/security/public-frontend-env-guard.py --root .``OK public frontend env guard`
- `python3 scripts/security/security-mirror-progress-guard.py --root .``SECURITY_MIRROR_PROGRESS_GUARD_OK`
- `python3 scripts/security/iwooos-config-control-guard.py --root .``IWOOOS_CONFIG_CONTROL_GUARD_OK`
- `python3 scripts/security/iwooos-owner-gate-guard.py --root .``IWOOOS_OWNER_GATE_GUARD_OK`
- `python3 scripts/security/package-supply-chain-owner-policy-guard.py --root .``PACKAGE_SUPPLY_CHAIN_OWNER_POLICY_GUARD_OK`
- `git diff --check` 通過。
**完成度與邊界**
- Public frontend env 範例防回歸:`0% -> 100%`
- Frontend public sensitive redaction defense`88% -> 89%`,只反映範例與 guard 覆蓋提升,不代表 runtime gate 開啟。
- IwoooS headline 維持 `64%`active runtime gate 維持 `0`
- S4.9 owner response、source-control owner acceptance、Nginx reload、host restart、firewall change、active scan、secret collection、runtime execution、action button 全部維持 `0 / false`
- 本輪未 SSH、未讀 live conf、未改主機、未重啟 Docker / Nginx、未修改 firewall / iptables、未收 secrets 明文、未執行 active scan、未 force push。
## 2026-06-15Monitoring / Alerting / Observability owner response acceptance 只讀帳本
**背景**Monitoring / Alerting / Observability 已有 repo-only 清冊與 owner request draft但仍缺少「owner 回覆收件後如何驗收、哪些欄位必填、哪些證據可接受、哪些動作必須阻擋」的固定帳本。這會讓 Prometheus / Alertmanager / Grafana / SigNoz / Sentry / Langfuse / OTEL / Telegram 告警鏈路在後續收件時,可能被誤判成 reload、receiver route change、silence change、Telegram send 或 alert chain smoke 授權。

View File

@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""檢查公開前端 env 範例不可洩漏內網拓樸。"""
from __future__ import annotations
import argparse
import re
from pathlib import Path
PRIVATE_IP_PATTERN = re.compile(
r"\b(?:10(?:\.\d{1,3}){3}|192\.168(?:\.\d{1,3}){2}|172\.(?:1[6-9]|2\d|3[01])(?:\.\d{1,3}){2})\b"
)
RETIRED_PUBLIC_TOPOLOGY_KEYS = {
"NEXT_PUBLIC_HOST_IPS",
"NEXT_PUBLIC_K8S_VIP_INFO",
}
ENV_EXAMPLE_PATHS = (
Path("apps/web/.env.example"),
)
def _active_assignment(line: str) -> tuple[str, str] | None:
stripped = line.strip()
if not stripped or stripped.startswith("#") or "=" not in stripped:
return None
key, value = stripped.split("=", 1)
return key.strip(), value.strip()
def validate(root: Path) -> None:
errors: list[str] = []
for relative_path in ENV_EXAMPLE_PATHS:
path = root / relative_path
if not path.exists():
continue
for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
assignment = _active_assignment(line)
if assignment is None:
continue
key, value = assignment
if key in RETIRED_PUBLIC_TOPOLOGY_KEYS:
errors.append(f"{relative_path}:{line_number}: 已停用公開前端拓樸 env key{key}")
if PRIVATE_IP_PATTERN.search(value):
errors.append(f"{relative_path}:{line_number}: env 範例不得包含內網 IP{key}")
if key.startswith("NEXT_PUBLIC_") and not value:
errors.append(f"{relative_path}:{line_number}: NEXT_PUBLIC_* 範例需明確使用公網入口或安全預設值:{key}")
if errors:
raise SystemExit("BLOCKED public frontend env guard:\n" + "\n".join(f"- {error}" for error in errors))
def main() -> None:
parser = argparse.ArgumentParser(description="檢查公開前端 env 範例不可洩漏內網拓樸")
parser.add_argument("--root", default=".", help="repository root")
args = parser.parse_args()
validate(Path(args.root).resolve())
print("OK public frontend env guard")
if __name__ == "__main__":
main()

View File

@@ -83,6 +83,10 @@ def validate(root: Path) -> None:
str(root / "scripts" / "security" / "package-supply-chain-owner-policy-guard.py")
)
package_policy_guard["validate"](root)
public_frontend_env_guard = runpy.run_path(
str(root / "scripts" / "security" / "public-frontend-env-guard.py")
)
public_frontend_env_guard["validate"](root)
manifest = load_json(security_dir / "security-supply-chain-contract-manifest.snapshot.json")
readiness = load_json(security_dir / "security-mirror-readiness.snapshot.json")