feat: add reproducible lighthouse node bootstrap

This commit is contained in:
冰朔 2026-07-17 12:29:40 +08:00
parent 67acd28cd8
commit 975ddbc42b
8 changed files with 266 additions and 7 deletions

View File

@ -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 公钥。

View File

@ -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

View File

@ -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}'

View File

@ -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"

View File

@ -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": []
}
}

View File

@ -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"

View File

@ -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/);
});

View File

@ -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=身份标识 · 操作=验证码授权 · 白名单=三元组校验');