diff --git a/deployment/navigation-maps/JD-FD-PRIMARY.json b/deployment/navigation-maps/JD-FD-PRIMARY.json index 5abc98f..ae6d051 100644 --- a/deployment/navigation-maps/JD-FD-PRIMARY.json +++ b/deployment/navigation-maps/JD-FD-PRIMARY.json @@ -10,7 +10,7 @@ { "code": "JD-SEN-01", "name": "状态变化哨兵", "bind": "timer:15m", "owner": "systemd" }, { "code": "JD-ROUTE-01", "name": "已登记下游节点 SSH 路由", "bind": "private-keys", "owner": "root", "registered_targets": ["AW-GZ-001"] }, { "code": "JD-ACT-01", "name": "固定动作执行桥", "bind": "unix-socket", "owner": "root", "actions": ["inspect-services", "health-check", "inspect-code-channel-owner-auth"] }, - { "code": "JD-OWNER-ACCESS-01", "name": "冰朔受限登录恢复执行器", "bind": "unix-socket", "owner": "root", "actions": ["restore-owner-password-login", "restore-code-channel-owner-login"], "action_meanings": { "restore-owner-password-login": "仅恢复京东服务器 SSH 密码认证开关,不修改代码频道账号", "restore-code-channel-owner-login": "仅将新代码频道 bingshuo 凭证恢复为旧第五域本机凭证,不读取明文密码" } }, + { "code": "JD-OWNER-ACCESS-01", "name": "冰朔与人格体受限登录执行器", "bind": "unix-socket", "owner": "root", "actions": ["inspect-owner-ssh-login", "disable-owner-password-login", "restore-owner-password-login", "restore-code-channel-owner-login"], "action_meanings": { "inspect-owner-ssh-login": "只回读 SSH 有效认证配置,不显示密钥或密码", "disable-owner-password-login": "关闭 SSH 密码与交互式认证,保留人格体工单使用的受控机器密钥通道", "restore-owner-password-login": "仅恢复京东服务器 SSH 密码认证开关,不修改代码频道账号", "restore-code-channel-owner-login": "仅将新代码频道 bingshuo 凭证恢复为旧第五域本机凭证,不读取明文密码" } }, { "code": "JD-ARCH-PROVISION-01", "name": "已批准新架构首次安装器", "bind": "unix-socket", "owner": "root", "actions": ["provision-approved-architecture"], "state": "ACTIVE" }, { "code": "JD-LAN-01", "name": "光湖·来光者导航只读召回服务", "bind": "loopback:3924", "owner": "systemd", "architecture": "GLS-0231", "state": "DEPLOYED_AND_VERIFIED", "source_commit": "f4a4b5996c83b55d99172f3196f4a0d77ed5f3e9" }, { diff --git a/server-tools/lake-lamp-authz/README.md b/server-tools/lake-lamp-authz/README.md index 4122d3a..3545145 100644 --- a/server-tools/lake-lamp-authz/README.md +++ b/server-tools/lake-lamp-authz/README.md @@ -96,6 +96,8 @@ node server-tools/lake-lamp-authz/authorize-repo-push.js \ ## 登录恢复动作不得混用 +- `inspect-owner-ssh-login` 只回读 SSH 的有效密码认证、交互式认证和 root 登录策略,不读取密钥或密码。 +- `disable-owner-password-login` 关闭 SSH 密码认证与交互式认证,并把 root 登录限制为仅密钥;人格体仍只能通过主人批准的工单调用固定动作,不能取得任意 shell。 - `restore-owner-password-login` 只恢复京东服务器的 SSH 密码认证开关;它不读取、不重置,也不验证光湖代码频道账号。 - `restore-code-channel-owner-login` 只把京东本机旧第五域数据库中的 `bingshuo` 密码摘要恢复到新代码频道,同时校正启用、管理员和禁止登录状态。它不读取明文密码,不修改 SSH,并在变更前使用 SQLite backup API 生成一致性备份。 - 登录问题先执行 `inspect-code-channel-owner-auth`。只有确认新旧频道凭证不一致时,才申请后一项恢复动作;不得绕到新加坡灾备节点,不得向冰朔索要密码。 diff --git a/server-tools/lake-lamp-authz/action-client.js b/server-tools/lake-lamp-authz/action-client.js index c635edf..619fb49 100644 --- a/server-tools/lake-lamp-authz/action-client.js +++ b/server-tools/lake-lamp-authz/action-client.js @@ -8,6 +8,8 @@ const ARCHITECTURE_PROVISION_SOCKET_PATH = process.env.LAKE_LAMP_ARCHITECTURE_PR function executeRegisteredAction(request, socketPath) { const ownerAccessActions = new Set([ + "inspect-owner-ssh-login", + "disable-owner-password-login", "restore-owner-password-login", "restore-code-channel-owner-login", ]); diff --git a/server-tools/lake-lamp-authz/owner-access-broker.js b/server-tools/lake-lamp-authz/owner-access-broker.js index 8e0061b..f5b438e 100644 --- a/server-tools/lake-lamp-authz/owner-access-broker.js +++ b/server-tools/lake-lamp-authz/owner-access-broker.js @@ -8,6 +8,62 @@ const { execFile } = require("node:child_process"); const SOCKET_PATH = process.env.LAKE_LAMP_OWNER_ACCESS_SOCKET || "/run/guanghu-owner-access/owner-access.sock"; const SSH_CONFIG = process.env.LAKE_LAMP_SSH_CONFIG || "/etc/guanghu/action-broker-ssh-config"; +const INSPECT_OWNER_SSH_LOGIN = String.raw`set -eu +test "$(id -u)" = "0" +password=$(/usr/sbin/sshd -T | awk '$1 == "passwordauthentication" {print $2; exit}') +keyboard=$(/usr/sbin/sshd -T | awk '$1 == "kbdinteractiveauthentication" {print $2; exit}') +root_login=$(/usr/sbin/sshd -T | awk '$1 == "permitrootlogin" {print $2; exit}') +printf 'PASSWORD_AUTH=%s\nKBD_INTERACTIVE_AUTH=%s\nROOT_LOGIN=%s\nSSH=%s\n' \ + "$password" "$keyboard" "$root_login" \ + "$(systemctl is-active ssh 2>/dev/null || systemctl is-active sshd)"`; + +const DISABLE_OWNER_PASSWORD_LOGIN = String.raw`set -eu +test "$(id -u)" = "0" +cfg=/etc/ssh/sshd_config.d/90-guanghu-key-only.conf +test -f "$cfg" +backup_dir=/root/guanghu-security-backup-$(date -u +%Y%m%dT%H%M%SZ) +mkdir -p "$backup_dir" +chmod 0700 "$backup_dir" +cp -a "$cfg" "$backup_dir/90-guanghu-key-only.conf.before" +rollback() { + cp -a "$backup_dir/90-guanghu-key-only.conf.before" "$cfg" + /usr/sbin/sshd -t + systemctl reload ssh 2>/dev/null || systemctl reload sshd +} +trap 'rc=$?; if [ "$rc" -ne 0 ]; then rollback; fi; exit "$rc"' EXIT +/usr/bin/python3 - "$cfg" <<'PY' +from pathlib import Path +import re, sys + +path = Path(sys.argv[1]) +text = path.read_text() +for directive, value in ( + ("PasswordAuthentication", "no"), + ("KbdInteractiveAuthentication", "no"), + ("PermitRootLogin", "prohibit-password"), +): + pattern = re.compile(rf"(?im)^\s*{directive}\s+\S+\s*$") + replacement = f"{directive} {value}" + if pattern.search(text): + text = pattern.sub(replacement, text) + else: + text = text.rstrip() + f"\n{replacement}\n" +path.write_text(text) +PY +/usr/sbin/sshd -t +password=$(/usr/sbin/sshd -T | awk '$1 == "passwordauthentication" {print $2; exit}') +keyboard=$(/usr/sbin/sshd -T | awk '$1 == "kbdinteractiveauthentication" {print $2; exit}') +root_login=$(/usr/sbin/sshd -T | awk '$1 == "permitrootlogin" {print $2; exit}') +test "$password" = "no" +test "$keyboard" = "no" +test "$root_login" != "yes" +systemctl reload ssh 2>/dev/null || systemctl reload sshd +printf 'PASSWORD_AUTH=%s\nKBD_INTERACTIVE_AUTH=%s\nROOT_LOGIN=%s\nSSH=%s\nBACKUP=%s\n' \ + "$password" "$keyboard" "$root_login" \ + "$(systemctl is-active ssh 2>/dev/null || systemctl is-active sshd)" \ + "$backup_dir/90-guanghu-key-only.conf.before" +trap - EXIT`; + const RESTORE_OWNER_PASSWORD_LOGIN = String.raw`set -eu test "$(id -u)" = "0" cfg=/etc/ssh/sshd_config.d/90-guanghu-key-only.conf @@ -171,6 +227,8 @@ async function execute(request) { if (!request || request.cmd || request.command || request.shell || request.args) return { ok: false, error: "arbitrary_command_forbidden" }; if (request.target !== "JD-FD-PRIMARY") return { ok: false, error: "action_not_registered" }; const commands = { + "inspect-owner-ssh-login": INSPECT_OWNER_SSH_LOGIN, + "disable-owner-password-login": DISABLE_OWNER_PASSWORD_LOGIN, "restore-owner-password-login": RESTORE_OWNER_PASSWORD_LOGIN, "restore-code-channel-owner-login": RESTORE_CODE_CHANNEL_OWNER_LOGIN, }; @@ -202,6 +260,8 @@ if (require.main === module) { module.exports = { execute, + INSPECT_OWNER_SSH_LOGIN, + DISABLE_OWNER_PASSWORD_LOGIN, RESTORE_OWNER_PASSWORD_LOGIN, RESTORE_CODE_CHANNEL_OWNER_LOGIN, SOCKET_PATH, diff --git a/server-tools/lake-lamp-authz/owner-access-broker.test.js b/server-tools/lake-lamp-authz/owner-access-broker.test.js index a012e7b..14f975a 100644 --- a/server-tools/lake-lamp-authz/owner-access-broker.test.js +++ b/server-tools/lake-lamp-authz/owner-access-broker.test.js @@ -4,11 +4,20 @@ const assert = require("node:assert/strict"); const test = require("node:test"); const { execute, + INSPECT_OWNER_SSH_LOGIN, + DISABLE_OWNER_PASSWORD_LOGIN, RESTORE_OWNER_PASSWORD_LOGIN, RESTORE_CODE_CHANNEL_OWNER_LOGIN, } = require("./owner-access-broker"); test("owner-access executor keeps SSH and code-channel recovery distinct", async () => { + assert.match(INSPECT_OWNER_SSH_LOGIN, /sshd -T/); + assert.doesNotMatch(INSPECT_OWNER_SSH_LOGIN, /write_text|systemctl reload/); + assert.match(DISABLE_OWNER_PASSWORD_LOGIN, /PasswordAuthentication", "no"/); + assert.match(DISABLE_OWNER_PASSWORD_LOGIN, /KbdInteractiveAuthentication", "no"/); + assert.match(DISABLE_OWNER_PASSWORD_LOGIN, /PermitRootLogin", "prohibit-password"/); + assert.match(DISABLE_OWNER_PASSWORD_LOGIN, /sshd -t/); + assert.match(DISABLE_OWNER_PASSWORD_LOGIN, /systemctl reload ssh/); assert.match(RESTORE_OWNER_PASSWORD_LOGIN, /PasswordAuthentication yes/); assert.match(RESTORE_OWNER_PASSWORD_LOGIN, /sshd -t/); assert.match(RESTORE_OWNER_PASSWORD_LOGIN, /systemctl reload ssh/); @@ -22,5 +31,6 @@ test("owner-access executor keeps SSH and code-channel recovery distinct", async assert.match(require("node:fs").readFileSync(require.resolve("./owner-access-broker"), "utf8"), /150000/); assert.deepEqual(await execute({ target: "JD-FD-PRIMARY", action: "unknown" }), { ok: false, error: "action_not_registered" }); assert.deepEqual(await execute({ target: "JD-FD-PRIMARY", action: "restore-owner-password-login", cmd: "id" }), { ok: false, error: "arbitrary_command_forbidden" }); + assert.deepEqual(await execute({ target: "JD-FD-PRIMARY", action: "disable-owner-password-login", args: ["--force"] }), { ok: false, error: "arbitrary_command_forbidden" }); assert.deepEqual(await execute({ target: "JD-FD-PRIMARY", action: "restore-code-channel-owner-login", shell: "id" }), { ok: false, error: "arbitrary_command_forbidden" }); }); diff --git a/server-tools/lake-lamp-authz/server.js b/server-tools/lake-lamp-authz/server.js index c53692a..73e1e17 100644 --- a/server-tools/lake-lamp-authz/server.js +++ b/server-tools/lake-lamp-authz/server.js @@ -15,6 +15,7 @@ const DEFAULT_ACTIONS = Object.freeze({ "read-navigation-map", "inspect-services", "health-check", + "inspect-owner-ssh-login", "inspect-code-channel-owner-auth", ], "server-ops": [ @@ -27,6 +28,8 @@ const DEFAULT_ACTIONS = Object.freeze({ "rollback-registered-service", "provision-approved-architecture", "push-repository", + "inspect-owner-ssh-login", + "disable-owner-password-login", "restore-owner-password-login", "restore-code-channel-owner-login", "dispatch-approved-deployment", diff --git a/server-tools/lake-lamp-authz/server.test.js b/server-tools/lake-lamp-authz/server.test.js index b42a585..2d1dd25 100644 --- a/server-tools/lake-lamp-authz/server.test.js +++ b/server-tools/lake-lamp-authz/server.test.js @@ -153,6 +153,8 @@ test("one server-ops approval exposes the full registered three-hour operation s "rollback-registered-service", "provision-approved-architecture", "push-repository", + "inspect-owner-ssh-login", + "disable-owner-password-login", "restore-owner-password-login", "restore-code-channel-owner-login", "dispatch-approved-deployment",