From b0b64ba07dc92e260cf4569025a1ee6e4c83cbdb Mon Sep 17 00:00:00 2001 From: oliver Date: Wed, 14 Oct 2026 15:28:21 +0800 Subject: [PATCH] Linux installer: catch paths the netx user cannot enter; --grant-access. Installing under /root left systemd failing with 203/EXEC and empty logs. The installer now checks that the service user can enter the program and data dirs; blocked parents are granted traverse-only access (setfacl u:netx:x, else chmod o+x) with --grant-access or after asking, otherwise it stops with the exact command. Start failures show the supervisor log or journalctl when api.log is empty. README: /root installs and UME with self-signed certificates (NETX_ALLOW_INSECURE_DEFAULTS). Co-Authored-By: Claude Opus 5.5 --- packaging/linux/README.md | 28 +++++++++++- packaging/linux/install.sh | 9 +++- packaging/linux/lib.sh | 91 ++++++++++++++++++++++++++++++++++++++ packaging/linux/netxctl | 2 +- 4 files changed, 126 insertions(+), 4 deletions(-) diff --git a/packaging/linux/README.md b/packaging/linux/README.md index 0431360..cdc357a 100644 --- a/packaging/linux/README.md +++ b/packaging/linux/README.md @@ -26,6 +26,20 @@ sudo ./install.sh sudo ./install.sh --prefix /data/netx --data /data/netx-data ``` +**目录在 `/root` 下面时**:NetX 服务以系统用户 `netx` 运行(内置 PostgreSQL 不允许用 root 运行),而 `/root` 默认不允许其他用户进入。安装程序会检测到这种情况:交互安装时询问是否授权;非交互安装加 `--grant-access` 自动授权。授权只给 `netx` 开放上级目录的"进入"权限(不能查看目录内容):优先用 `setfacl -m u:netx:x`,系统没有 ACL 工具时用 `chmod o+x`。 + +```bash +sudo ./install.sh --prefix /root/service/netx/programfile --data /root/service/netx/userdata --grant-access +``` + +已经装在 `/root` 下、启动失败且日志为空的,手动授权后直接 `sudo netxctl start` 即可,不用重装: + +```bash +setfacl -m u:netx:x /root /root/service /root/service/netx +``` + +卸载时不会撤销这些授权;需要的话用 `setfacl -x u:netx <目录>` 撤销。 + 全部安装参数见 `./install.sh --help`。 交互式安装会依次询问:监听地址(默认 `0.0.0.0`)、**Web/API 端口**(默认 8890,被占用会提示重新输入)、数据库(内置 / 外置)、**内置 PostgreSQL 端口**(默认 15432,只监听 127.0.0.1)。 @@ -39,7 +53,19 @@ sudo ./install.sh --yes --db external --db-url 'postgresql+psycopg://netx:密码 装好后访问 `http://<服务器IP>:<端口>/`,初始账号 `admin`,密码在安装结束时显示(随机生成,也可用 `--admin-password` 指定;同时保存在 `/var/lib/netx/.env` 的 `NETX_BOOTSTRAP_ADMIN_PASSWORD`),首次登录必须改密码。 -> 对外监听(非 127.0.0.1)时 NetX 自身要求:管理员初始密码不能是 admin123,并且开启 UME 的 TLS 校验。安装程序会自动设置 `NETX_UME_VERIFY_TLS=true`;如果 UME 用的是自签名证书,需要配置证书,或者改成只监听本机再通过反向代理访问。 +> 对外监听(非 127.0.0.1)时 NetX 自身要求:管理员初始密码不能是 admin123,并且开启 UME 的 TLS 校验,否则拒绝启动(api.log 末尾是 `security_bootstrap.py ... SystemExit: 2`)。安装程序会自动生成随机管理员密码,并设置 `NETX_UME_VERIFY_TLS=true`。 +> +> **对接 UME(通常是自签名证书)**:在 `/var/lib/netx/.env` 中加入下面几行,然后 `sudo netxctl restart`: +> +> ``` +> NETX_UME_BASE_URL=https://:<端口> +> NETX_UME_USERNAME=... +> NETX_UME_PASSWORD=... +> NETX_UME_VERIFY_TLS=false +> NETX_ALLOW_INSECURE_DEFAULTS=1 +> ``` +> +> `NETX_ALLOW_INSECURE_DEFAULTS=1` 会跳过上面这组启动检查。由于管理员密码和 JWT 密钥在安装时已经单独生成,实际放开的只有"访问 UME 时不校验证书"这一项(与 Windows 版默认行为相同)。 | 路径 | 内容 | |---|---| diff --git a/packaging/linux/install.sh b/packaging/linux/install.sh index 7e61e2a..bd14598 100755 --- a/packaging/linux/install.sh +++ b/packaging/linux/install.sh @@ -33,6 +33,7 @@ NO_SERVICE=0 AUTO_UPDATE=0 OPEN_FIREWALL=0 NO_BACKUP=0 +GRANT_ACCESS=0 YES=0 usage() { @@ -55,6 +56,7 @@ $(t "用法" "Usage"): sudo ./install.sh [options] --auto-update $(t "开启每日自动更新" "enable daily automatic updates") --open-firewall $(t "自动放行防火墙端口(firewalld/ufw)" "open the port in firewalld/ufw") --no-backup $(t "更新前不备份数据库" "skip the pre-update database dump") + --grant-access $(t "目录在 /root 等受限目录下时,自动给服务用户 netx 开放上级目录的“进入”权限" "if a parent dir (e.g. /root) blocks the netx service user, grant it traverse-only access") -y, --yes $(t "非交互,使用默认值" "non-interactive, accept defaults") EOF } @@ -81,6 +83,7 @@ while [[ $# -gt 0 ]]; do --auto-update) AUTO_UPDATE=1; shift ;; --open-firewall) OPEN_FIREWALL=1; shift ;; --no-backup) NO_BACKUP=1; shift ;; + --grant-access) GRANT_ACCESS=1; shift ;; -y|--yes) YES=1; shift ;; -h|--help) usage; exit 0 ;; *) err "unknown option: $1"; usage; exit 2 ;; @@ -524,6 +527,7 @@ summary() { do_install() { # install or reinstall ensure_user + check_netx_access "${PREFIX}" "${DATA}" mkdir -p "${DATA}" ensure_data_dirs "${DATA}" if [[ -x "${PREFIX}/current/packaging/linux/netxctl" ]]; then @@ -539,7 +543,7 @@ do_install() { # install or reinstall if [[ "${AUTO_UPDATE}" == "1" ]]; then "${PREFIX}/current/packaging/linux/netxctl" auto-update on || true; fi if ! start_and_wait; then err "$(t "NetX 启动失败,日志" "NetX failed to start; logs"): ${DATA}/logs/" - tail -n 30 "${DATA}/logs/api.log" >&2 2>/dev/null || true + show_start_failure "${DATA}" exit 1 fi prune_releases "" @@ -549,6 +553,7 @@ do_install() { # install or reinstall do_update() { [[ -f "${ENV_FILE}" ]] || die "$(t "未找到已有安装(${ENV_FILE}),请用 --mode install" "no existing install (${ENV_FILE}); use --mode install")" ensure_user + check_netx_access "${PREFIX}" "${DATA}" ensure_data_dirs "${DATA}" local old_ver="" if [[ -f "${PREFIX}/current/version.json" ]]; then @@ -585,7 +590,7 @@ do_update() { return 0 fi err "$(t "新版本启动失败" "new version failed to start")" - tail -n 30 "${DATA}/logs/api.log" >&2 2>/dev/null || true + show_start_failure "${DATA}" svc_stop "${PREFIX}" "${DATA}" local back="" if [[ -n "${old_ver}" && "${old_ver}" != "${VERSION}" && -d "${PREFIX}/releases/${old_ver}" ]]; then diff --git a/packaging/linux/lib.sh b/packaging/linux/lib.sh index 6dff3fc..fa37df2 100644 --- a/packaging/linux/lib.sh +++ b/packaging/linux/lib.sh @@ -81,6 +81,97 @@ client_host() { esac } +# The service runs as ${NETX_USER}: it must be able to enter the program and data +# dirs. Typical failure: paths under /root (mode 550/700) -> systemd 203/EXEC and +# empty logs, because netxctl never runs. +netx_can_enter() { as_netx /bin/sh -c 'cd "$1"' sh "$1" 2>/dev/null; } + +# Let NETX_USER traverse one directory: ACL for that user only, else o+x. +grant_traverse() { + local p="$1" + if command -v setfacl >/dev/null 2>&1 && setfacl -m "u:${NETX_USER}:x" "${p}" 2>/dev/null; then + info "setfacl -m u:${NETX_USER}:x ${p}" + else + chmod o+x "${p}" && info "chmod o+x ${p}" + fi +} + +# Can NETX_USER pass through this dir judging by mode bits alone? (Used only to +# name the blocking dirs below an already blocked one, which cannot be probed.) +others_can_traverse() { + local m + m="$(stat -c %A "$1" 2>/dev/null)" || return 1 + [[ "${m:9:1}" == "x" || "${m:9:1}" == "t" ]] +} + +# check_netx_access DIR... — every dir must be enterable by NETX_USER. +# Blocked parents (typically /root) are granted traverse-only access when +# GRANT_ACCESS=1 or the interactive user agrees; otherwise abort with the commands. +# (Plain strings, not arrays: empty arrays trip `set -u` on bash 4.2 / CentOS 7.) +check_netx_access() { + local d p rest comp ans blocked="" seen_block + for d in "$@"; do + mkdir -p "${d}" + netx_can_enter "${d}" && continue + p="" + seen_block=0 + rest="${d#/}" + while [[ -n "${rest}" ]]; do + comp="${rest%%/*}" + if [[ "${rest}" == */* ]]; then rest="${rest#*/}"; else rest=""; fi + [[ -n "${comp}" ]] || continue + p="${p}/${comp}" + if [[ "${GRANT_ACCESS:-0}" == "1" ]]; then + if ! netx_can_enter "${p}"; then + grant_traverse "${p}" + blocked="${blocked} ${p}" + fi + elif [[ "${seen_block}" == "0" ]]; then + if ! netx_can_enter "${p}"; then + seen_block=1 + [[ " ${blocked} " == *" ${p} "* ]] || blocked="${blocked} ${p}" + fi + elif ! others_can_traverse "${p}" && [[ " ${blocked} " != *" ${p} "* ]]; then + blocked="${blocked} ${p}" + fi + done + done + blocked="${blocked# }" + [[ -n "${blocked}" ]] || return 0 + if [[ "${GRANT_ACCESS:-0}" != "1" ]]; then + warn "$(t "服务用户 ${NETX_USER} 无法进入以下目录(常见于 /root 下面)" "service user ${NETX_USER} cannot enter these directories (typically under /root)"): ${blocked}" + if [[ "${INTERACTIVE:-0}" == "1" ]]; then + read -r -p "$(t "只给 ${NETX_USER} 开放这些目录的“进入”权限(不能查看内容)?[Y/n] " "Grant ${NETX_USER} traverse-only access to them? [Y/n] ")" ans || true + if [[ ! "${ans}" =~ ^[Nn] ]]; then + GRANT_ACCESS=1 + check_netx_access "$@" + return + fi + fi + die "$(t "请加 --grant-access 自动授权,或手动执行:setfacl -m u:${NETX_USER}:x ${blocked}(没有 setfacl 时用 chmod o+x),或换到 /root 以外的目录" "re-run with --grant-access, or run: setfacl -m u:${NETX_USER}:x ${blocked} (chmod o+x without setfacl), or use a path outside /root")" + fi + for d in "$@"; do + netx_can_enter "${d}" || die "$(t "授权后 ${NETX_USER} 仍无法进入" "still not enterable by ${NETX_USER} after granting"): ${d}" + done + ok "$(t "已授权 ${NETX_USER} 进入程序/数据目录" "granted ${NETX_USER} access to the program/data directories")" +} + +# Show why NetX did not come up: app log first, else supervisor log, else systemd. +show_start_failure() { + local data="$1" f + for f in "${data}/logs/api.log" "${data}/logs/netxctl.log"; do + if [[ -s "${f}" ]]; then + echo "--- ${f}" >&2 + tail -n 30 "${f}" >&2 + return 0 + fi + done + if svc_unit_installed; then + echo "--- journalctl -u netx" >&2 + journalctl -u netx.service -n 30 --no-pager >&2 2>/dev/null || systemctl status netx.service --no-pager >&2 || true + fi +} + is_loopback() { case "$1" in 127.0.0.1|localhost|::1) return 0 ;; *) return 1 ;; esac; } primary_ip() { diff --git a/packaging/linux/netxctl b/packaging/linux/netxctl index d27d88e..73525af 100755 --- a/packaging/linux/netxctl +++ b/packaging/linux/netxctl @@ -72,7 +72,7 @@ wait_healthy() { return 0 fi err "$(t "NetX 未在 ${timeout} 秒内就绪" "NetX not healthy after ${timeout}s"): $(health_url)" - tail -n 30 "${NETX_LOG_DIR}/api.log" 2>/dev/null >&2 || true + show_start_failure "${NETX_DATA}" return 1 }