From 975ddbc42b00fc120ffaa19d71e3d8bdbc725e78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=B0=E6=9C=94?= <565183519@qq.com> Date: Fri, 17 Jul 2026 12:29:40 +0800 Subject: [PATCH] feat: add reproducible lighthouse node bootstrap --- .../light-lake-node-bootstrap/README.md | 37 +++++++++ .../guanghu-gatekeeper.service | 26 +++++++ .../light-lake-node-bootstrap/health-check.sh | 20 +++++ .../light-lake-node-bootstrap/install.sh | 76 +++++++++++++++++++ .../node.example.json | 19 +++++ .../light-lake-node-bootstrap/rollback.sh | 18 +++++ .../test/bootstrap.test.js | 61 +++++++++++++++ .../core-channel/gatekeeper/engine-v3.js | 16 ++-- 8 files changed, 266 insertions(+), 7 deletions(-) create mode 100644 server-tools/light-lake-node-bootstrap/README.md create mode 100644 server-tools/light-lake-node-bootstrap/guanghu-gatekeeper.service create mode 100755 server-tools/light-lake-node-bootstrap/health-check.sh create mode 100755 server-tools/light-lake-node-bootstrap/install.sh create mode 100644 server-tools/light-lake-node-bootstrap/node.example.json create mode 100755 server-tools/light-lake-node-bootstrap/rollback.sh create mode 100644 server-tools/light-lake-node-bootstrap/test/bootstrap.test.js diff --git a/server-tools/light-lake-node-bootstrap/README.md b/server-tools/light-lake-node-bootstrap/README.md new file mode 100644 index 0000000..66cf0cf --- /dev/null +++ b/server-tools/light-lake-node-bootstrap/README.md @@ -0,0 +1,37 @@ +# 光湖灯塔节点初始化模板 + +所有接入光湖灯塔体系的服务器使用同一套可重复、可审计、可回滚的基础环境。京东云北京 +`JD-FD-PRIMARY / JD-OPS-CENTER` 是第一台真实样板节点。 + +## 固定基线 + +- Ubuntu Server 22.04 LTS、x86_64; +- Node.js 20、Python 3、Git、jq、Docker、Nginx(默认停止,配置完成前不开放 80/443); +- `/opt/guanghu` 放只读部署代码,`/etc/guanghu` 放节点配置和受保护 secrets; +- `/var/lib/guanghu` 放运行状态与回执,`/var/backups/guanghu` 放部署前快照; +- Gatekeeper v3.2 仅监听 `127.0.0.1:3911`,不直接暴露公网; +- 每个节点拥有独立 `node_id`、独立 SSH key_id、独立地图与回执链。 + +## 安装 + +在一台全新的 Ubuntu 22.04 节点上,以 root 执行: + +```bash +NODE_ID=JD-FD-PRIMARY \ +NODE_NAME='京东云北京 · 第五域国内主节点' \ +NODE_ROLES='fd-primary,ops-center,lighthouse-node' \ +bash server-tools/light-lake-node-bootstrap/install.sh +``` + +脚本可重复执行。重复部署会先把节点配置和 systemd 单元备份到 +`/var/backups/guanghu/bootstrap/`。 + +## 验收与回滚 + +```bash +guanghu-node-health +guanghu-node-rollback +``` + +健康检查必须同时证明节点身份、Gatekeeper systemd 状态、私网健康端点、仓库提交和磁盘状态。 +回滚不删除 secrets,不清空运行数据,也不修改 SSH 公钥。 diff --git a/server-tools/light-lake-node-bootstrap/guanghu-gatekeeper.service b/server-tools/light-lake-node-bootstrap/guanghu-gatekeeper.service new file mode 100644 index 0000000..d1403d9 --- /dev/null +++ b/server-tools/light-lake-node-bootstrap/guanghu-gatekeeper.service @@ -0,0 +1,26 @@ +[Unit] +Description=Guanghu Gatekeeper v3.2 (GLSV) +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=guanghu +Group=guanghu +WorkingDirectory=/opt/guanghu/fifth-domain/zero-point/core-channel/gatekeeper +Environment=NODE_ENV=production +Environment=ENGINE_HOST=127.0.0.1 +Environment=ENGINE_PORT=3911 +Environment=GATEKEEPER_DATA_DIR=/var/lib/guanghu/gatekeeper +EnvironmentFile=-/etc/guanghu/gatekeeper.env +ExecStart=/usr/bin/node /opt/guanghu/fifth-domain/zero-point/core-channel/gatekeeper/engine-v3.js +Restart=always +RestartSec=3 +NoNewPrivileges=true +PrivateTmp=true +ProtectHome=true +ProtectSystem=strict +ReadWritePaths=/var/lib/guanghu/gatekeeper /var/log/guanghu + +[Install] +WantedBy=multi-user.target diff --git a/server-tools/light-lake-node-bootstrap/health-check.sh b/server-tools/light-lake-node-bootstrap/health-check.sh new file mode 100755 index 0000000..2f0ce0e --- /dev/null +++ b/server-tools/light-lake-node-bootstrap/health-check.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail + +NODE_FILE=${NODE_FILE:-/etc/guanghu/node.json} +REPO_DIR=${REPO_DIR:-/opt/guanghu/fifth-domain} +HEALTH_URL=${HEALTH_URL:-http://127.0.0.1:3911/health} + +node_id=$(jq -r '.node_id' "$NODE_FILE") +service_state=$(systemctl is-active guanghu-gatekeeper.service) +health=$(curl -fsS "$HEALTH_URL") +commit=$(git -C "$REPO_DIR" rev-parse --short HEAD) +disk_used=$(df --output=pcent / | tail -1 | tr -d ' ') + +jq -n \ + --arg node_id "$node_id" \ + --arg service "$service_state" \ + --arg commit "$commit" \ + --arg disk_used "$disk_used" \ + --argjson gatekeeper "$health" \ + '{ok:true,node_id:$node_id,service:$service,repo_commit:$commit,disk_used:$disk_used,gatekeeper:$gatekeeper}' diff --git a/server-tools/light-lake-node-bootstrap/install.sh b/server-tools/light-lake-node-bootstrap/install.sh new file mode 100755 index 0000000..4362e68 --- /dev/null +++ b/server-tools/light-lake-node-bootstrap/install.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Guanghu lighthouse-node baseline: Ubuntu 22.04 / x86_64 / Node 20. +NODE_ID=${NODE_ID:-JD-FD-PRIMARY} +NODE_NAME=${NODE_NAME:-京东云北京 · 第五域国内主节点} +NODE_ROLES=${NODE_ROLES:-fd-primary,ops-center,lighthouse-node} +PROVIDER=${PROVIDER:-jdcloud} +REGION=${REGION:-beijing} +REPO_URL=${REPO_URL:-https://guanghubingshuo.com/code/bingshuo/fifth-domain.git} +REPO_BRANCH=${REPO_BRANCH:-main} +REPO_DIR=${REPO_DIR:-/opt/guanghu/fifth-domain} +BACKUP_DIR=/var/backups/guanghu/bootstrap/$(date -u +%Y%m%dT%H%M%SZ) + +if [[ $EUID -ne 0 ]]; then echo "Run as root" >&2; exit 1; fi +. /etc/os-release +[[ ${ID:-} == ubuntu && ${VERSION_ID:-} == 22.04 ]] || { echo "Ubuntu 22.04 required" >&2; exit 1; } +[[ $(uname -m) == x86_64 ]] || { echo "x86_64 required" >&2; exit 1; } +[[ $NODE_ID =~ ^[A-Z0-9][A-Z0-9-]+$ ]] || { echo "Invalid NODE_ID" >&2; exit 1; } + +mkdir -p "$BACKUP_DIR" +[[ -f /etc/guanghu/node.json ]] && cp -a /etc/guanghu/node.json "$BACKUP_DIR/node.json" +[[ -f /etc/guanghu/gatekeeper.env ]] && cp -a /etc/guanghu/gatekeeper.env "$BACKUP_DIR/gatekeeper.env" +[[ -f /etc/systemd/system/guanghu-gatekeeper.service ]] && cp -a /etc/systemd/system/guanghu-gatekeeper.service "$BACKUP_DIR/guanghu-gatekeeper.service" + +export DEBIAN_FRONTEND=noninteractive +apt-get update +apt-get install -y ca-certificates curl git jq rsync unzip gnupg docker.io nginx python3 python3-venv +systemctl disable --now nginx.service || true +systemctl enable --now docker.service + +node_major=$(node -p 'process.versions.node.split(".")[0]' 2>/dev/null || echo 0) +if (( node_major < 20 )); then + install -d -m 0755 /etc/apt/keyrings + curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor --yes -o /etc/apt/keyrings/nodesource.gpg + echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" > /etc/apt/sources.list.d/nodesource.list + apt-get update + apt-get install -y nodejs +fi + +id -u guanghu >/dev/null 2>&1 || useradd --system --home /var/lib/guanghu --create-home --shell /usr/sbin/nologin guanghu +mkdir -p /opt/guanghu /etc/guanghu/secrets /var/lib/guanghu/gatekeeper /var/lib/guanghu/receipts /var/log/guanghu +chmod 0700 /etc/guanghu/secrets /var/lib/guanghu/gatekeeper +chown -R guanghu:guanghu /var/lib/guanghu /var/log/guanghu + +if [[ -d "$REPO_DIR/.git" ]]; then + git -C "$REPO_DIR" fetch --prune origin "$REPO_BRANCH" + git -C "$REPO_DIR" reset --hard "origin/$REPO_BRANCH" +else + rm -rf "$REPO_DIR" + git clone --filter=blob:none --branch "$REPO_BRANCH" "$REPO_URL" "$REPO_DIR" +fi + +roles_json=$(printf '%s' "$NODE_ROLES" | jq -Rc 'split(",") | map(select(length > 0))') +jq -n \ + --arg node_id "$NODE_ID" --arg display_name "$NODE_NAME" \ + --arg provider "$PROVIDER" --arg region "$REGION" --argjson roles "$roles_json" \ + '{schema:"guanghu.node/v1",node_id:$node_id,display_name:$display_name,owner:"ICE-GL∞",controller_persona:"ICE-GL-ZY001",provider:$provider,region:$region,roles:$roles,runtime:{os:"Ubuntu 22.04",arch:"x86_64",gatekeeper_bind:"127.0.0.1:3911"},credentials:{policy:"private material stays in /etc/guanghu/secrets",key_ids:[]}}' \ + > /etc/guanghu/node.json +chmod 0640 /etc/guanghu/node.json +chown root:guanghu /etc/guanghu/node.json + +if [[ ! -f /etc/guanghu/gatekeeper.env ]]; then + printf 'GATEKEEPER_SERVER_ID=%s\n' "$NODE_ID" > /etc/guanghu/gatekeeper.env +fi +chmod 0640 /etc/guanghu/gatekeeper.env +chown root:guanghu /etc/guanghu/gatekeeper.env + +install -m 0644 "$REPO_DIR/server-tools/light-lake-node-bootstrap/guanghu-gatekeeper.service" /etc/systemd/system/guanghu-gatekeeper.service +install -m 0755 "$REPO_DIR/server-tools/light-lake-node-bootstrap/health-check.sh" /usr/local/sbin/guanghu-node-health +install -m 0755 "$REPO_DIR/server-tools/light-lake-node-bootstrap/rollback.sh" /usr/local/sbin/guanghu-node-rollback +systemctl daemon-reload +systemctl enable --now guanghu-gatekeeper.service + +"$REPO_DIR/server-tools/light-lake-node-bootstrap/health-check.sh" | tee "/var/lib/guanghu/receipts/bootstrap-$(date -u +%Y%m%dT%H%M%SZ).json" +echo "Guanghu lighthouse node baseline installed: $NODE_ID" diff --git a/server-tools/light-lake-node-bootstrap/node.example.json b/server-tools/light-lake-node-bootstrap/node.example.json new file mode 100644 index 0000000..881f324 --- /dev/null +++ b/server-tools/light-lake-node-bootstrap/node.example.json @@ -0,0 +1,19 @@ +{ + "schema": "guanghu.node/v1", + "node_id": "JD-FD-PRIMARY", + "display_name": "京东云北京 · 第五域国内主节点", + "owner": "ICE-GL∞", + "controller_persona": "ICE-GL-ZY001", + "provider": "jdcloud", + "region": "beijing", + "roles": ["fd-primary", "ops-center", "lighthouse-node"], + "runtime": { + "os": "Ubuntu 22.04", + "arch": "x86_64", + "gatekeeper_bind": "127.0.0.1:3911" + }, + "credentials": { + "policy": "private material stays in /etc/guanghu/secrets", + "key_ids": [] + } +} diff --git a/server-tools/light-lake-node-bootstrap/rollback.sh b/server-tools/light-lake-node-bootstrap/rollback.sh new file mode 100755 index 0000000..06fa2eb --- /dev/null +++ b/server-tools/light-lake-node-bootstrap/rollback.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -euo pipefail + +BACKUP_ROOT=${BACKUP_ROOT:-/var/backups/guanghu/bootstrap} +BACKUP_DIR=${1:-$(find "$BACKUP_ROOT" -mindepth 1 -maxdepth 1 -type d | sort | tail -1)} + +if [[ -z "${BACKUP_DIR:-}" || ! -d "$BACKUP_DIR" ]]; then + echo "No bootstrap backup found" >&2 + exit 1 +fi + +systemctl stop guanghu-gatekeeper.service 2>/dev/null || true +[[ -f "$BACKUP_DIR/node.json" ]] && install -m 0640 "$BACKUP_DIR/node.json" /etc/guanghu/node.json +[[ -f "$BACKUP_DIR/gatekeeper.env" ]] && install -m 0640 "$BACKUP_DIR/gatekeeper.env" /etc/guanghu/gatekeeper.env +[[ -f "$BACKUP_DIR/guanghu-gatekeeper.service" ]] && install -m 0644 "$BACKUP_DIR/guanghu-gatekeeper.service" /etc/systemd/system/guanghu-gatekeeper.service +systemctl daemon-reload +systemctl start guanghu-gatekeeper.service 2>/dev/null || true +echo "Rolled back from $BACKUP_DIR" diff --git a/server-tools/light-lake-node-bootstrap/test/bootstrap.test.js b/server-tools/light-lake-node-bootstrap/test/bootstrap.test.js new file mode 100644 index 0000000..ae8b2de --- /dev/null +++ b/server-tools/light-lake-node-bootstrap/test/bootstrap.test.js @@ -0,0 +1,61 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const path = require("node:path"); +const test = require("node:test"); + +const ROOT = path.resolve(__dirname, "../../.."); +const MODULE = path.join(ROOT, "server-tools/light-lake-node-bootstrap"); + +function read(relative) { + return fs.readFileSync(path.join(MODULE, relative), "utf8"); +} + +test("bootstrap declares the reproducible Guanghu node baseline", () => { + const source = read("install.sh"); + for (const marker of [ + "Ubuntu 22.04", + "x86_64", + "NODE_ID", + "/etc/guanghu/node.json", + "/var/lib/guanghu/gatekeeper", + "guanghu-gatekeeper.service", + "node_20.x", + "health-check.sh", + ]) assert.match(source, new RegExp(marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))); +}); + +test("bootstrap never opens Gatekeeper publicly", () => { + const install = read("install.sh"); + const unit = read("guanghu-gatekeeper.service"); + assert.match(unit, /ENGINE_HOST=127\.0\.0\.1/); + assert.doesNotMatch(install, /ufw\s+allow\s+3911/); + assert.doesNotMatch(install, /0\.0\.0\.0:3911/); +}); + +test("template keeps secrets out of repository configuration", () => { + const config = JSON.parse(read("node.example.json")); + assert.equal(config.schema, "guanghu.node/v1"); + assert.ok(Array.isArray(config.roles)); + assert.equal("password" in config, false); + assert.equal("private_key" in config, false); + assert.equal("token" in config, false); +}); + +test("Gatekeeper runtime accepts explicit node identity, host and data directory", () => { + const engine = fs.readFileSync(path.join(ROOT, "zero-point/core-channel/gatekeeper/engine-v3.js"), "utf8"); + assert.match(engine, /process\.env\.GATEKEEPER_SERVER_ID/); + assert.match(engine, /process\.env\.ENGINE_HOST/); + assert.match(engine, /process\.env\.GATEKEEPER_DATA_DIR/); +}); + +test("rollback and health verification are part of the template", () => { + const rollback = read("rollback.sh"); + const health = read("health-check.sh"); + assert.match(rollback, /systemctl/); + assert.match(rollback, /BACKUP_DIR/); + assert.match(health, /127\.0\.0\.1:3911\/health/); + assert.match(health, /curl -fsS/); + assert.match(health, /systemctl is-active/); +}); diff --git a/zero-point/core-channel/gatekeeper/engine-v3.js b/zero-point/core-channel/gatekeeper/engine-v3.js index 2f9aee2..dd3fc02 100644 --- a/zero-point/core-channel/gatekeeper/engine-v3.js +++ b/zero-point/core-channel/gatekeeper/engine-v3.js @@ -28,7 +28,8 @@ const { SessionManager } = require('./session-manager'); const { getAction, listActions } = require('./authorized-actions'); const PORT = parseInt(process.env.ENGINE_PORT || process.env.GATEKEEPER_PORT || process.argv[2] || '3911', 10); -const DATA_DIR = path.join('/opt/zhuyuan', 'gatekeeper'); +const HOST = process.env.ENGINE_HOST || '0.0.0.0'; +const DATA_DIR = process.env.GATEKEEPER_DATA_DIR || path.join('/opt/zhuyuan', 'gatekeeper'); const CMD_TIMEOUT = 30000, MAX_OUTPUT = 100000; const CODE_TTL = 300; // 验证码 5 分钟过期 const RATE_LIMIT_WINDOW = 60; @@ -73,6 +74,7 @@ const HOSTNAME = os.hostname(); // 服务器标识映射 // ═══════════════════════════════════════ function getServerId() { + if (process.env.GATEKEEPER_SERVER_ID) return process.env.GATEKEEPER_SERVER_ID; const map = { 'guanghu-lang-sg-001': 'BS-SG-001', 'VM-0-16-ubuntu': 'BS-GZ-006', @@ -143,8 +145,8 @@ function log(level, action, detail) { function loadWhitelist() { const wf = path.join(DATA_DIR, 'whitelist.json'); const required = [ - { server:'BS-SG-001', human:'ICE-GL∞', email:process.env.BINGSHUO_AUTH_EMAIL || '', personalities:['ICE-GL-ZL-001'], bound_servers:['BS-SG-001'] }, - { server:'BS-SG-001', human:'苍耳', human_id:'TCS-GL-009', email:process.env.CANGER_AUTH_EMAIL || '', personalities:['PTS-VA-001-EED','ICE-GL-CA001'], bound_servers:['BS-SG-001'] } + { server:SERVER_ID, human:'ICE-GL∞', email:process.env.BINGSHUO_AUTH_EMAIL || '', personalities:['ICE-GL-ZL-001'], bound_servers:[SERVER_ID] }, + { server:SERVER_ID, human:'苍耳', human_id:'TCS-GL-009', email:process.env.CANGER_AUTH_EMAIL || '', personalities:['PTS-VA-001-EED','ICE-GL-CA001'], bound_servers:[SERVER_ID] } ]; if (fs.existsSync(wf)) { try { @@ -165,8 +167,8 @@ function loadWhitelist() { // 默认白名单:冰朔 + 之之 return { triads: [ - { server: 'BS-SG-001', human: 'ICE-GL∞', email: process.env.BINGSHUO_AUTH_EMAIL || '', personalities: ['ICE-GL-ZY001','ICE-GL-ZL-001'], bound_servers: ['BS-SG-001'] }, - { server: 'BS-SG-001', human: '苍耳', human_id: 'TCS-GL-009', email: process.env.CANGER_AUTH_EMAIL || '', personalities: ['PTS-VA-001-EED','ICE-GL-CA001'], bound_servers: ['BS-SG-001'] }, + { server: SERVER_ID, human: 'ICE-GL∞', email: process.env.BINGSHUO_AUTH_EMAIL || '', personalities: ['ICE-GL-ZY001','ICE-GL-ZL-001'], bound_servers: [SERVER_ID] }, + { server: SERVER_ID, human: '苍耳', human_id: 'TCS-GL-009', email: process.env.CANGER_AUTH_EMAIL || '', personalities: ['PTS-VA-001-EED','ICE-GL-CA001'], bound_servers: [SERVER_ID] }, ], pending_ops: {} }; @@ -854,8 +856,8 @@ server.on('error', (e) => { } }); -server.listen(PORT, '0.0.0.0', () => { - log('INFO', 'startup', 'host=' + HOSTNAME + ' port=' + PORT + ' v3.2.0 server_id=' + SERVER_ID); +server.listen(PORT, HOST, () => { + log('INFO', 'startup', 'host=' + HOSTNAME + ' bind=' + HOST + ' port=' + PORT + ' v3.2.0 server_id=' + SERVER_ID); console.log(' ⚔️ 光湖驱动引擎 v3.2 · 工作会话授权 · 已就绪'); console.log(' 主机名: ' + HOSTNAME + ' 标识: ' + SERVER_ID + ' 端口: ' + PORT); console.log(' 安全策略: Token=身份标识 · 操作=验证码授权 · 白名单=三元组校验');