docs(web): 鎖住公開前端 env 範例拓樸 [skip ci]
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# API 後端 URL(Next.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 自建主機 URL(sentry-tunnel route handler 使用)
|
||||
SENTRY_HOST=http://192.168.0.110:9000
|
||||
SENTRY_HOST=https://sentry.wooo.work
|
||||
|
||||
@@ -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-15|Monitoring / 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 授權。
|
||||
|
||||
67
scripts/security/public-frontend-env-guard.py
Normal file
67
scripts/security/public-frontend-env-guard.py
Normal 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()
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user