From f71c2779a83221404b3eb050e24d797f109e4866 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 13 Jun 2026 02:01:13 +0800 Subject: [PATCH] fix(web): redact public host targets --- apps/web/src/app/[locale]/classic/page.tsx | 47 ++++++++++++++----- apps/web/src/app/[locale]/iwooos/page.tsx | 10 ++-- apps/web/src/app/api/sentry-tunnel/route.ts | 18 ++++--- .../components/dashboard/live-dashboard.tsx | 20 +++++--- .../components/dashboard/live-host-card.tsx | 24 ++++++++-- apps/web/src/components/infra/host-grid.tsx | 27 +++++++++-- docs/LOGBOOK.md | 23 +++++++++ 7 files changed, 132 insertions(+), 37 deletions(-) diff --git a/apps/web/src/app/[locale]/classic/page.tsx b/apps/web/src/app/[locale]/classic/page.tsx index a8a3707b7..7eb366d4c 100644 --- a/apps/web/src/app/[locale]/classic/page.tsx +++ b/apps/web/src/app/[locale]/classic/page.tsx @@ -247,8 +247,30 @@ function MonitoringTools() { // 定義每台主機完整服務清單(API 只回傳部分,此處補全靜態資訊) // ============================================================================= +const HOST_ALIAS_BY_LAST_OCTET: Record = { + '110': 'host:public-gateway', + '111': 'host:dev-a', + '112': 'host:kali-readonly', + '120': 'host:k3s-control-a', + '121': 'host:k3s-control-b', + '125': 'host:edge-vip', + '168': 'host:dev-b', + '188': 'host:observability-a', +} + +function toHostAlias(address: string): string { + const match = address.trim().match(/^(?:\d{1,3}\.){3}(\d{1,3})$/) + if (!match) return address + return HOST_ALIAS_BY_LAST_OCTET[match[1]] ?? 'host:internal-node' +} + +function toPublicHostname(hostname: string, fallbackAlias: string): string { + if (/^(?:\d{1,3}\.){3}\d{1,3}$/.test(hostname.trim())) return fallbackAlias + return hostname +} + const HOST_CATALOG: Record = { - '192.168.0.110': { + 'host:public-gateway': { services: [ { name: 'Harbor', healthy: false, port: 5000, description: 'Container Registry' }, { name: 'Gitea', healthy: false, port: 3001, description: 'Git · CI/CD' }, @@ -258,12 +280,12 @@ const HOST_CATALOG: Record { const dyn = dynamicServices.find(d => d.name.toLowerCase() === s.name.toLowerCase()) @@ -317,8 +340,8 @@ function buildHostInfo( healthy: s.status === 'up' || s.status === 'healthy', })) return { - hostname, - ip, + hostname: toPublicHostname(hostname, hostAlias), + ip: hostAlias, cpuPct, ramPct, services, @@ -780,10 +803,10 @@ export default function Home({ params }: { params: { locale: string } }) { const apiHosts = hosts.map(h => buildHostInfo(h.ip, h.name, h.metrics?.cpu_percent ?? null, h.metrics?.memory_percent ?? null, h.services) ) - // K3s #2 (121) 若 API 未回傳,補靜態卡 - const has121 = apiHosts.some(h => h.ip === '192.168.0.121') - if (!has121) { - apiHosts.push(buildHostInfo('192.168.0.121', 'K3s Server #2', null, null, [])) + // 控制平面 B 若 API 未回傳,補靜態卡。 + const hasControlB = apiHosts.some(h => h.ip === 'host:k3s-control-b') + if (!hasControlB) { + apiHosts.push(buildHostInfo('host:k3s-control-b', 'K3s Server #2', null, null, [])) } return apiHosts })()} /> diff --git a/apps/web/src/app/[locale]/iwooos/page.tsx b/apps/web/src/app/[locale]/iwooos/page.tsx index 2b2627077..cbd4d5f8e 100644 --- a/apps/web/src/app/[locale]/iwooos/page.tsx +++ b/apps/web/src/app/[locale]/iwooos/page.tsx @@ -3277,7 +3277,7 @@ const iwooosTopologyDrilldownNodes: IwoooSTopologyDrilldownNode[] = [ }, { key: 'kali', - relationCode: '192.168.0.112', + relationCode: 'host:kali-112', evidenceCode: '只讀觀測', nextCode: '範圍批准', boundaryCode: '掃描 0', @@ -3286,7 +3286,7 @@ const iwooosTopologyDrilldownNodes: IwoooSTopologyDrilldownNode[] = [ }, { key: 'devHosts', - relationCode: '192.168.0.111 / 168', + relationCode: 'host:dev-111 / host:dev-168', evidenceCode: '主機窗口 0', nextCode: '負責人窗口', boundaryCode: '變更 0', @@ -4541,9 +4541,9 @@ const evidenceReadinessItems: EvidenceReadinessItem[] = [ ] const hostCoverageItems: HostCoverageItem[] = [ - { key: 'kali112', address: '192.168.0.112', icon: Activity, tone: 'steady' }, - { key: 'dev168', address: '192.168.0.168', icon: Radar, tone: 'warn' }, - { key: 'dev111', address: '192.168.0.111', icon: Radar, tone: 'warn' }, + { key: 'kali112', address: 'host:kali-112', icon: Activity, tone: 'steady' }, + { key: 'dev168', address: 'host:dev-168', icon: Radar, tone: 'warn' }, + { key: 'dev111', address: 'host:dev-111', icon: Radar, tone: 'warn' }, ] const kaliMaintenanceReadinessItems: KaliMaintenanceReadinessItem[] = [ diff --git a/apps/web/src/app/api/sentry-tunnel/route.ts b/apps/web/src/app/api/sentry-tunnel/route.ts index 29efe8c3d..5cb8fd56d 100644 --- a/apps/web/src/app/api/sentry-tunnel/route.ts +++ b/apps/web/src/app/api/sentry-tunnel/route.ts @@ -2,7 +2,7 @@ * Sentry Tunnel API Route * ======================= * - * 解決問題: 前端 Sentry DSN 使用內網 IP (192.168.0.110:9000) 會觸發 + * 解決問題: 前端 Sentry DSN 使用內網 origin 會觸發 * 瀏覽器「存取區域網路上的其他裝置」權限對話框。 * * 解決方案: 使用 Next.js API Route 作為 Tunnel,前端透過公網域名 @@ -16,10 +16,8 @@ import { type NextRequest, NextResponse } from 'next/server'; -// Sentry Self-Hosted 內網地址 -// 2026-04-22 ogt: 改為讀 env var,避免內網 IP 硬碼進 bundle。 -// K8s: awoooi-secrets → SENTRY_HOST;本機 dev fallback 維持原值不中斷。 -const SENTRY_HOST = process.env.SENTRY_HOST ?? 'http://192.168.0.110:9000'; +// Sentry Self-Hosted 目標只允許由 runtime env 明確注入;不可在 source 留私網 fallback。 +const SENTRY_HOST = process.env.SENTRY_HOST?.trim() ?? ''; // 允許的 Project IDs (防止濫用) const ALLOWED_PROJECT_IDS = new Set(['2', '3']); // awoooi-web: 2, awoooi-api: 3 @@ -51,6 +49,14 @@ export async function POST(request: NextRequest) { ); } + if (!SENTRY_HOST) { + console.error('[Sentry Tunnel] Missing SENTRY_HOST'); + return NextResponse.json( + { error: 'Sentry tunnel target is not configured' }, + { status: 503 } + ); + } + // 轉發到 Sentry Server const response = await fetch(`${SENTRY_HOST}/api/${projectId}/envelope/`, { method: 'POST', @@ -83,6 +89,6 @@ export async function GET() { return NextResponse.json({ status: 'ok', tunnel: '/api/sentry-tunnel', - target: SENTRY_HOST, + target_configured: Boolean(SENTRY_HOST), }); } diff --git a/apps/web/src/components/dashboard/live-dashboard.tsx b/apps/web/src/components/dashboard/live-dashboard.tsx index b51496947..b4c096bef 100644 --- a/apps/web/src/components/dashboard/live-dashboard.tsx +++ b/apps/web/src/components/dashboard/live-dashboard.tsx @@ -46,7 +46,12 @@ const _getApiBaseUrl = () => { return url } -const HOST_IPS = (process.env.NEXT_PUBLIC_HOST_IPS ?? '192.168.0.110,192.168.0.112,192.168.0.120,192.168.0.188').split(',') +const HOST_SLOT_IDS = [ + 'host:public-gateway', + 'host:kali-readonly', + 'host:k3s-control-a', + 'host:observability-a', +] // ============================================================================= // Component @@ -70,7 +75,7 @@ export function LiveDashboard({ locale: _locale }: LiveDashboardProps) { // Host fallback data with i18n const HOST_FALLBACKS: Record }> = { - '192.168.0.110': { + 'host:public-gateway': { name: tHost('devops.name'), role: 'devops', services: [ @@ -79,7 +84,7 @@ export function LiveDashboard({ locale: _locale }: LiveDashboardProps) { { name: 'Docker', status: 'idle', port: 2375 }, ], }, - '192.168.0.112': { + 'host:kali-readonly': { name: tHost('security.name'), role: 'security', services: [ @@ -88,7 +93,7 @@ export function LiveDashboard({ locale: _locale }: LiveDashboardProps) { { name: 'Nuclei', status: 'idle' }, ], }, - '192.168.0.120': { + 'host:k3s-control-a': { name: tHost('k3s.name'), role: 'k3s', services: [ @@ -97,7 +102,7 @@ export function LiveDashboard({ locale: _locale }: LiveDashboardProps) { { name: 'Traefik', status: 'idle', port: 80 }, ], }, - '192.168.0.188': { + 'host:observability-a': { name: tHost('aiWeb.name'), role: 'ai_web', services: [ @@ -118,6 +123,9 @@ export function LiveDashboard({ locale: _locale }: LiveDashboardProps) { // return () => disconnect() // }, [connect, disconnect]) + const liveHostKeys = Array.from(new Set(hosts.map((host) => host.ip).filter(Boolean))) + const hostCardKeys = liveHostKeys.length > 0 ? liveHostKeys : HOST_SLOT_IDS + return (
{/* Header Stats */} @@ -139,7 +147,7 @@ export function LiveDashboard({ locale: _locale }: LiveDashboardProps) { {/* Four Host Cards */}
- {HOST_IPS.map((ip) => ( + {hostCardKeys.map((ip) => ( = { + '110': 'host:public-gateway', + '111': 'host:dev-a', + '112': 'host:kali-readonly', + '120': 'host:k3s-control-a', + '121': 'host:k3s-control-b', + '125': 'host:edge-vip', + '168': 'host:dev-b', + '188': 'host:observability-a', +} + +function toPublicHostId(value: string): string { + const match = value.trim().match(/^(?:\d{1,3}\.){3}(\d{1,3})$/) + if (!match) return value + return HOST_ALIAS_BY_LAST_OCTET[match[1]] ?? 'host:internal-node' +} + // ============================================================================= // Component // ============================================================================= @@ -91,6 +108,7 @@ export function LiveHostCard({ const tDashboard = useTranslations('dashboard') const tCommon = useTranslations('common') const host = useHostByIp(ip) + const publicHostId = toPublicHostId(ip) // Track status changes for flash effect const [isFlashing, setIsFlashing] = useState(false) @@ -113,7 +131,7 @@ export function LiveHostCard({
- {tCommon('loading')} {ip} + {tCommon('loading')} {publicHostId}
@@ -127,7 +145,7 @@ export function LiveHostCard({
- {ip} + {publicHostId}
@@ -199,7 +217,7 @@ export function LiveHostCard({
- {host.ip} + {toPublicHostId(host.ip)}
diff --git a/apps/web/src/components/infra/host-grid.tsx b/apps/web/src/components/infra/host-grid.tsx index 93f9c56d9..dcb721545 100644 --- a/apps/web/src/components/infra/host-grid.tsx +++ b/apps/web/src/components/infra/host-grid.tsx @@ -32,6 +32,23 @@ export interface HostGridProps { hosts: HostInfo[] } +const HOST_ALIAS_BY_LAST_OCTET: Record = { + '110': 'host:public-gateway', + '111': 'host:dev-a', + '112': 'host:kali-readonly', + '120': 'host:k3s-control-a', + '121': 'host:k3s-control-b', + '125': 'host:edge-vip', + '168': 'host:dev-b', + '188': 'host:observability-a', +} + +function toPublicHostId(value: string): string { + const match = value.trim().match(/^(?:\d{1,3}\.){3}(\d{1,3})$/) + if (!match) return value + return HOST_ALIAS_BY_LAST_OCTET[match[1]] ?? 'host:internal-node' +} + function MetricBar({ value, warn = 80, critical = 90 }: { value: number | null; warn?: number; critical?: number }) { if (value === null) return -- const color = value >= critical ? '#cc2200' : value >= warn ? '#F59E0B' : '#22C55E' @@ -61,6 +78,7 @@ function HostCard({ host }: { host: HostInfo }) { const borderColor = isK3s ? '#93c5fd' : '#e0ddd4' const headerBg = isK3s ? '#eff6ff' : '#f5f4ed' const accentColor = isK3s ? '#3b7de8' : '#d97757' + const publicHostId = toPublicHostId(host.ip) return (
- {/* 顯示末段 IP 作為簡短標識,完整名稱放 IP 欄位 */} - {host.ip.includes('.') ? host.ip.split('.').pop() ?? host.ip : host.hostname} + {publicHostId.startsWith('host:') ? publicHostId.replace('host:', '') : host.hostname} {isK3s && ( )}
- {host.ip} + {publicHostId}
CPU @@ -118,7 +135,7 @@ function HostCard({ host }: { host: HostInfo }) { } const K8S_VIP_INFO_FALLBACK = - 'K8S VIP topology (ops-only) · kubectl:6443 · web:32335 · api:32334' + 'K8S HA topology redacted · kubectl:6443 · web:32335 · api:32334' export function HostGrid({ hosts }: HostGridProps) { if (hosts.length === 0) { @@ -153,7 +170,7 @@ export function HostGrid({ hosts }: HostGridProps) { ☸ K3S CLUSTER (HA) - {(process.env.NEXT_PUBLIC_K8S_VIP_INFO ?? '').trim() || K8S_VIP_INFO_FALLBACK} + {K8S_VIP_INFO_FALLBACK}
diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 027b86d53..2216743a3 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,26 @@ +## 2026-06-13|P0-PUBLICENV client/API source redaction production patch + +**01:46 / 02:00 source patch:** +- 在乾淨 worktree `/tmp/awoooi-publicenv-prod-20260613-014604`、基底 `gitea/main=6a8b9f5c` 上補前端 public env / host display redaction;未使用 dirty main tree 的前端缺檔狀態。 +- `apps/web/src/app/[locale]/classic/page.tsx`:legacy host catalog key、API host render value 與 K3s control-plane fallback 改為 `host:*` 資產代號。 +- `apps/web/src/app/[locale]/iwooos/page.tsx`:IwoooS topology / host coverage 中的 dev / Kali host address 改為 `host:*` 資產代號。 +- `apps/web/src/components/dashboard/live-dashboard.tsx`:移除 `NEXT_PUBLIC_HOST_IPS` 讀取與 private-IP fallback,改用公開 host slot ids;runtime store 有資料時仍以 store host key 產生卡片。 +- `apps/web/src/components/dashboard/live-host-card.tsx`:顯示前把 runtime host IP 轉成 `host:*`,避免卡片直接顯示內網 IP。 +- `apps/web/src/components/infra/host-grid.tsx`:共用 HostGrid 顯示層加入 runtime IP redaction,移除 `NEXT_PUBLIC_K8S_VIP_INFO` 讀取,固定顯示 redacted K8s topology 文案。 +- `apps/web/src/app/api/sentry-tunnel/route.ts`:移除 server-side `SENTRY_HOST` 私網 fallback;runtime 未明確注入時 POST 以 `503` fail-closed,GET 健康檢查只回 `target_configured` boolean,不再回顯 target。 + +**驗證(本地)**: +- Targeted source scan:上述五個前端檔與 `api/sentry-tunnel` 不再命中 `192.168.0.110 / 111 / 112 / 120 / 121 / 125 / 168 / 188`、`NEXT_PUBLIC_HOST_IPS`、`NEXT_PUBLIC_K8S_VIP_INFO`、`SENTRY_HOST ??`、`target: SENTRY_HOST`、`NEXT_PUBLIC_SENTRY_DSN`、`工作視窗`、`批准!`、`source_thread_id`、`codex_delegation`。 +- `pnpm --filter @awoooi/web typecheck` 通過。 +- `NEXT_PUBLIC_API_URL=https://awoooi.wooo.work NEXT_PRIVATE_BUILD_WORKER_COUNT=1 SENTRY_SUPPRESS_GLOBAL_ERROR_HANDLER_FILE_WARNING=1 pnpm --filter @awoooi/web build`:Next compile 成功、static pages `92/92` 完成;本機最後在 `collect-build-traces` 因磁碟剩餘空間不足 `ENOSPC` 失敗。這是本機磁碟限制,不是 TypeScript / compile error。 +- 部分 build 產物掃描:`apps/web/.next/static` 與 generated HTML / RSC 未命中本輪 private IP / public env / 工作視窗字串。 +- `scripts/ops/doc-secrets-sanity-check.py docs .gitea` 通過,`DOC_SECRET_SANITY_OK scanned_files=728`;`source-control-owner-response-guard.py --root .` 與 `security-mirror-progress-guard.py --root .` 通過。 + +**仍需 owner review / production gate:** +- `NEXT_PUBLIC_SENTRY_DSN` 舊 workflow / runtime env cleanup 與 `SENTRY_HOST` runtime 注入證據仍需 owner review;本輪只改 source fail-closed 與 public GET redaction,不直接改 workflow / runtime secret / deployment。 +- `k8s/awoooi-prod/05-deployment-web.yaml` 仍保留 legacy `NEXT_PUBLIC_HOST_IPS` / `NEXT_PUBLIC_K8S_VIP_INFO` env;前端 source 已不讀取,manifest cleanup 需 owner review。 +- 不能宣稱 production remediation complete,直到 Gitea CD build/deploy 成功、production bundle rescan、desktop/mobile Browser smoke、overflow 與工作視窗內容外洩檢查全部通過。 + ## 2026-06-13|Post-CD cold-start green 與 SSH trust guardrail closeout **01:26 / 01:29 live refresh:**