security(jd): add persona-only SSH closure actions

This commit is contained in:
冰朔 2026-07-27 14:33:03 +08:00
commit 7c3c8e9238
7 changed files with 80 additions and 1 deletions

View file

@ -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`。只有确认新旧频道凭证不一致时,才申请后一项恢复动作;不得绕到新加坡灾备节点,不得向冰朔索要密码。

View file

@ -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",
]);

View file

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

View file

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

View file

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

View file

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