From eb0eccdffe955770fef926a7a648e9af5c9ca0d7 Mon Sep 17 00:00:00 2001 From: oliver Date: Wed, 14 Oct 2026 15:50:38 +0800 Subject: [PATCH] UME TLS off by default on Linux and Windows; truthful admin login hints. - Deployments are on internal networks with self-signed UME certificates: installers write NETX_UME_VERIFY_TLS=false and NETX_ALLOW_INSECURE_DEFAULTS=1 when absent (Linux also on update), so binding 0.0.0.0 no longer stops NetX at the startup security check. Admin password and JWT secret stay unique per install. - Linux: after install, check app_user to report whether the generated initial password applies or the admin already exists (reused external DB); new 'netxctl admin-password'. Co-Authored-By: Claude Opus 5.5 --- packaging/linux/README.md | 26 +++++++++++++------------- packaging/linux/install.sh | 18 +++++++++++------- packaging/linux/lib.sh | 23 +++++++++++++++++++++++ packaging/linux/netx_admin.py | 32 ++++++++++++++++++++++++++++++++ packaging/linux/netxctl | 10 +++++++++- packaging/linux/smoke_test.sh | 2 ++ packaging/setup_first_run.ps1 | 5 +++++ 7 files changed, 95 insertions(+), 21 deletions(-) diff --git a/packaging/linux/README.md b/packaging/linux/README.md index cdc357a..688d488 100644 --- a/packaging/linux/README.md +++ b/packaging/linux/README.md @@ -53,19 +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 校验,否则拒绝启动(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 版默认行为相同)。 +**登录密码**:安装结束时会显示登录信息,之后也可以随时用 `sudo netxctl admin-password` 查看。 +- 新数据库:NetX 首次启动时用随机生成的初始密码创建 `admin`(保存在 `.env` 的 `NETX_BOOTSTRAP_ADMIN_PASSWORD`),首次登录必须修改。 +- 沿用已有数据库(例如外置库里已有以前部署的数据):库里已经有账号,初始密码不会生效,请用原来的密码登录;`admin-password` 会明确提示这种情况。 + +**对接 UME**:在 `/var/lib/netx/.env` 中加入下面几行,然后 `sudo netxctl restart`: + +``` +NETX_UME_BASE_URL=https://:<端口> +NETX_UME_USERNAME=... +NETX_UME_PASSWORD=... +``` + +> UME 一般使用自签名证书,所以安装程序默认设置 `NETX_UME_VERIFY_TLS=false` 和 `NETX_ALLOW_INSECURE_DEFAULTS=1`。后者用来放行 NetX 的启动安全检查(对外监听时,该检查要求开启 UME 证书校验、管理员初始密码不是 admin123;否则 api.log 末尾是 `security_bootstrap.py ... SystemExit: 2`)。管理员密码和 JWT 密钥在安装时已经单独生成,所以实际放开的只有"访问 UME 时不校验证书"这一项,与 Windows 版默认行为相同。如果 UME 证书可信,可以改成 `NETX_UME_VERIFY_TLS=true` 并删掉 `NETX_ALLOW_INSECURE_DEFAULTS`。 | 路径 | 内容 | |---|---| diff --git a/packaging/linux/install.sh b/packaging/linux/install.sh index bd14598..4459711 100755 --- a/packaging/linux/install.sh +++ b/packaging/linux/install.sh @@ -361,9 +361,12 @@ write_env() { ADMIN_PW_GENERATED=1 pairs+=("NETX_BOOTSTRAP_ADMIN_PASSWORD=${ADMIN_PASSWORD}") fi - if ! is_loopback "${BIND}" && [[ -z "$(env_get "${ENV_FILE}" NETX_UME_VERIFY_TLS)" ]]; then - pairs+=("NETX_UME_VERIFY_TLS=true") - fi + # Deployments are on internal networks where UME uses self-signed certificates: + # TLS verification is off by default and that check is waived explicitly + # (admin password and JWT secret are already unique per install). Written for + # every bind address so a later `netxctl set-bind` cannot trip the check. + [[ -n "$(env_get "${ENV_FILE}" NETX_UME_VERIFY_TLS)" ]] || pairs+=("NETX_UME_VERIFY_TLS=false") + [[ -n "$(env_get "${ENV_FILE}" NETX_ALLOW_INSECURE_DEFAULTS)" ]] || pairs+=("NETX_ALLOW_INSECURE_DEFAULTS=1") if [[ -n "${CRED_KEY}" ]]; then pairs+=("NETX_CREDENTIAL_SECRET_KEY=${CRED_KEY}") elif [[ -z "$(env_get "${ENV_FILE}" NETX_CREDENTIAL_SECRET_KEY)" ]]; then @@ -507,10 +510,7 @@ summary() { echo ok "NetX ${VERSION} $(t "已就绪" "is up")" echo " $(t "访问地址" "URL") : http://${host}:${port}/" - if [[ "${MODE}" == "install" && -n "${ADMIN_PASSWORD}" ]]; then - echo " $(t "初始账号" "first login"): admin / ${ADMIN_PASSWORD} ($(t "首次登录后必须修改密码" "you must change it at first login"))" - [[ "${ADMIN_PW_GENERATED}" == "1" ]] && echo " $(t "(随机生成,只显示这一次;也保存在" "(generated; shown once, also kept in") ${ENV_FILE} NETX_BOOTSTRAP_ADMIN_PASSWORD)" - fi + [[ "${MODE}" == "update" ]] || print_admin_login "${ENV_FILE}" echo " $(t "程序目录" "program") : ${PREFIX}/current -> releases/${VERSION}" echo " $(t "数据目录" "data") : ${DATA}" echo " $(t "管理命令" "manage") : sudo netxctl status | stop | start | set-port | set-pg-port | update" @@ -543,6 +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/" + echo " $(t "排除问题后执行 sudo netxctl start;登录密码用 sudo netxctl admin-password 查看" "after fixing it: sudo netxctl start; login password: sudo netxctl admin-password")" >&2 show_start_failure "${DATA}" exit 1 fi @@ -578,6 +579,9 @@ do_update() { # Re-assert keys newer releases rely on; never touches ports or DB settings. local pairs=("NETX_UI_DIST_DIR=${PREFIX}/current/web/dist" "NETX_RUN_INLINE_SCHEDULERS=false") kv while IFS= read -r kv; do pairs+=("${kv}"); done < <(data_env_pairs "${DATA}") + # Defaults added after the first Linux release; existing values are kept. + [[ -n "$(env_get "${ENV_FILE}" NETX_UME_VERIFY_TLS)" ]] || pairs+=("NETX_UME_VERIFY_TLS=false") + [[ -n "$(env_get "${ENV_FILE}" NETX_ALLOW_INSECURE_DEFAULTS)" ]] || pairs+=("NETX_ALLOW_INSECURE_DEFAULTS=1") netx_admin env-set "${ENV_FILE}" "${pairs[@]}" chown "${NETX_USER}:${NETX_USER}" "${ENV_FILE}" switch_current "${VERSION}" diff --git a/packaging/linux/lib.sh b/packaging/linux/lib.sh index fa37df2..b2b78ec 100644 --- a/packaging/linux/lib.sh +++ b/packaging/linux/lib.sh @@ -172,6 +172,29 @@ show_start_failure() { fi } +# print_admin_login ENV_FILE — tell the truth about the admin login: the generated +# initial password only applies when NetX created the admin itself. +print_admin_login() { + local out state user pw + out="$(netx_admin admin-status --env "$1" 2>/dev/null)" || return 0 + state="$(printf '%s +' "${out}" | sed -n 's/^state=//p')" + user="$(printf '%s +' "${out}" | sed -n 's/^username=//p')" + pw="$(printf '%s +' "${out}" | sed -n 's/^password=//p')" + case "${state}" in + initial) + echo " $(t "登录账号" "login") : ${user} / ${pw} ($(t "初始密码,首次登录后必须修改" "initial password; change it at first login"))" ;; + changed) + echo " $(t "登录账号" "login") : ${user} $(t "已有自己的密码(改过密码,或沿用了旧数据库里的账号),请用原来的密码登录" "already has its own password (changed, or reused from an existing database): use that one")" ;; + none) + echo " $(t "登录账号" "login") : ${user} / ${pw} ($(t "首次启动建表后生效,首次登录后必须修改" "takes effect at first start; change it at first login"))" ;; + *) + echo " $(t "登录账号" "login") : $(t "暂时无法读取数据库,稍后执行 sudo netxctl admin-password 查看" "database not readable right now; run sudo netxctl admin-password later")" ;; + esac +} + is_loopback() { case "$1" in 127.0.0.1|localhost|::1) return 0 ;; *) return 1 ;; esac; } primary_ip() { diff --git a/packaging/linux/netx_admin.py b/packaging/linux/netx_admin.py index a7d4473..3d9e2cb 100644 --- a/packaging/linux/netx_admin.py +++ b/packaging/linux/netx_admin.py @@ -611,6 +611,37 @@ def cmd_tar_list(a: argparse.Namespace) -> None: print(name) +def cmd_admin_status(a: argparse.Namespace) -> None: + """Does the bootstrap admin still use the initial password from .env? + + Prints `state` (initial | changed | none | unknown), `username`, `password` + as KEY=VALUE lines. `changed` also covers an admin that already existed in a + reused database: NetX only applies the bootstrap password to an empty app_user. + """ + env = read_env(Path(a.env)) + user = (env.get("NETX_BOOTSTRAP_ADMIN_USERNAME") or "admin").strip() or "admin" + pw = (env.get("NETX_BOOTSTRAP_ADMIN_PASSWORD") or "admin123").strip() or "admin123" + state = "unknown" + try: + import bcrypt # type: ignore + import psycopg # type: ignore + + p = url_parts(env.get("NETX_DATABASE_URL", "")) + with psycopg.connect(host=p["host"], port=int(p["port"]), user=p["user"], password=p["password"], + dbname=p["db"], connect_timeout=8) as conn: + row = conn.execute("SELECT password_hash FROM app_user WHERE username = %s", (user,)).fetchone() + if row is None: + state = "none" + else: + ok = bcrypt.checkpw(pw.encode("utf-8"), str(row[0] or "").encode("ascii")) + state = "initial" if ok else "changed" + except Exception as exc: # table missing before first start, DB down, ... + state = "none" if "app_user" in str(exc) and "does not exist" in str(exc) else "unknown" + print(f"state={state}") + print(f"username={user}") + print(f"password={pw}") + + def cmd_check_update(a: argparse.Namespace) -> None: cfg = Cfg(read_env(Path(a.env))) cur = current_version(Path(a.prefix)) @@ -710,6 +741,7 @@ def main(argv: list[str] | None = None) -> None: s = sub.add_parser("extract"); s.add_argument("file"); s.add_argument("--workdir", required=True) s.add_argument("--arch", default="x86_64"); s.set_defaults(fn=cmd_extract) + s = sub.add_parser("admin-status"); s.add_argument("--env", required=True); s.set_defaults(fn=cmd_admin_status) s = sub.add_parser("run-as"); s.add_argument("user"); s.add_argument("argv", nargs=argparse.REMAINDER) s.set_defaults(fn=cmd_run_as) s = sub.add_parser("spawn"); s.add_argument("log"); s.add_argument("pidfile") diff --git a/packaging/linux/netxctl b/packaging/linux/netxctl index 73525af..a5b57b1 100755 --- a/packaging/linux/netxctl +++ b/packaging/linux/netxctl @@ -43,6 +43,7 @@ $(t "用法" "Usage"): netxctl [options] update [--yes] [--file ] [--force] auto-update on|off $(t "每日自动更新" "daily automatic update") backup [] $(t "备份数据库与密钥" "back up database + secrets") + admin-password $(t "查看管理员登录密码情况" "show the admin login (initial password or not)") uninstall [--purge] [--yes] version run $(t "前台运行(systemd 使用)" "foreground supervisor (used by systemd)") @@ -235,6 +236,11 @@ cmd_logs() { cmd_version() { cur_version; } +cmd_admin_password() { + need_root admin-password; need_install + print_admin_login "${ENV_FILE}" +} + # ------------------------------------------------------------------ ports # apply_env_change KEY=VAL... ; restarts if running, rolls back on failure. @@ -295,7 +301,8 @@ cmd_set_bind() { if ! is_loopback "${h}"; then [[ -n "$(cfg NETX_BOOTSTRAP_ADMIN_PASSWORD)" && "$(cfg NETX_BOOTSTRAP_ADMIN_PASSWORD)" != "admin123" ]] || pairs+=("NETX_BOOTSTRAP_ADMIN_PASSWORD=$(netx_admin gen-secret password)") - [[ -n "$(cfg NETX_UME_VERIFY_TLS)" ]] || pairs+=("NETX_UME_VERIFY_TLS=true") + [[ -n "$(cfg NETX_UME_VERIFY_TLS)" ]] || pairs+=("NETX_UME_VERIFY_TLS=false") + [[ -n "$(cfg NETX_ALLOW_INSECURE_DEFAULTS)" ]] || pairs+=("NETX_ALLOW_INSECURE_DEFAULTS=1") fi apply_env_change set_bind "${pairs[@]}" ok "$(t "监听地址已改为" "listen address is now") ${h}:$(api_port)" @@ -518,6 +525,7 @@ case "${cmd}" in backup) cmd_backup "$@" ;; uninstall) cmd_uninstall "$@" ;; version|--version|-V) cmd_version ;; + admin-password) cmd_admin_password ;; ""|-h|--help|help) usage ;; *) err "unknown command: ${cmd}"; usage; exit 2 ;; esac diff --git a/packaging/linux/smoke_test.sh b/packaging/linux/smoke_test.sh index 39ffc5c..d473b02 100755 --- a/packaging/linux/smoke_test.sh +++ b/packaging/linux/smoke_test.sh @@ -56,6 +56,8 @@ listening "${PG1}" || fail "PG not on ${PG1}" token="$(login "${PORT1}")" || fail "login" [[ -n "${token}" ]] || fail "empty token" netxctl status || fail "status" +netxctl admin-password | grep "$(grep '^NETX_BOOTSTRAP_ADMIN_PASSWORD=' /var/lib/netx/.env | cut -d= -f2-)" >/dev/null || fail "admin-password (initial)" +grep -q '^NETX_ALLOW_INSECURE_DEFAULTS=1' /var/lib/netx/.env || fail "UME/insecure defaults not set" step "marker row survives update" PGPASSWORD="$(grep '^NETX_DATABASE_URL=' /var/lib/netx/.env | sed 's#.*://netx:\([^@]*\)@.*#\1#')" \ diff --git a/packaging/setup_first_run.ps1 b/packaging/setup_first_run.ps1 index 07a2a41..387c643 100644 --- a/packaging/setup_first_run.ps1 +++ b/packaging/setup_first_run.ps1 @@ -63,6 +63,11 @@ $values = @{} $values["NETX_DB_MODE"] = $DbMode $values["NETX_HOST"] = "127.0.0.1" $values["NETX_PORT"] = "8890" +# Internal networks: UME uses self-signed certificates. Keep TLS verification off and +# waive that startup check explicitly, so switching NETX_HOST to 0.0.0.0 later does +# not stop NetX (same defaults as the Linux installer). Existing values are kept. +if (-not $existing.ContainsKey("NETX_UME_VERIFY_TLS")) { $values["NETX_UME_VERIFY_TLS"] = "false" } +if (-not $existing.ContainsKey("NETX_ALLOW_INSECURE_DEFAULTS")) { $values["NETX_ALLOW_INSECURE_DEFAULTS"] = "1" } # Absolute UI path when present; else relative for source trees. $distAbs = Join-Path $prog "web\dist" if (Test-Path (Join-Path $distAbs "index.html")) {