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 <noreply@anthropic.com>
This commit is contained in:
oliver 2026-10-14 15:50:38 +08:00
parent b0b64ba07d
commit eb0eccdffe
7 changed files with 95 additions and 21 deletions

View file

@ -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`),首次登录必须改密码。 装好后访问 `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`。 **登录密码**:安装结束时会显示登录信息,之后也可以随时用 `sudo netxctl admin-password` 查看。
> - 新数据库:NetX 首次启动时用随机生成的初始密码创建 `admin`(保存在 `.env` 的 `NETX_BOOTSTRAP_ADMIN_PASSWORD`),首次登录必须修改。
> **对接 UME(通常是自签名证书)**:在 `/var/lib/netx/.env` 中加入下面几行,然后 `sudo netxctl restart`: - 沿用已有数据库(例如外置库里已有以前部署的数据):库里已经有账号,初始密码不会生效,请用原来的密码登录;`admin-password` 会明确提示这种情况。
>
> ``` **对接 UME**:在 `/var/lib/netx/.env` 中加入下面几行,然后 `sudo netxctl restart`:
> NETX_UME_BASE_URL=https://<UME地址>:<端口>
> NETX_UME_USERNAME=... ```
> NETX_UME_PASSWORD=... NETX_UME_BASE_URL=https://<UME地址>:<端口>
> NETX_UME_VERIFY_TLS=false NETX_UME_USERNAME=...
> NETX_ALLOW_INSECURE_DEFAULTS=1 NETX_UME_PASSWORD=...
> ``` ```
>
> `NETX_ALLOW_INSECURE_DEFAULTS=1` 会跳过上面这组启动检查。由于管理员密码和 JWT 密钥在安装时已经单独生成,实际放开的只有"访问 UME 时不校验证书"这一项(与 Windows 版默认行为相同)。 > 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`。
| 路径 | 内容 | | 路径 | 内容 |
|---|---| |---|---|

View file

@ -361,9 +361,12 @@ write_env() {
ADMIN_PW_GENERATED=1 ADMIN_PW_GENERATED=1
pairs+=("NETX_BOOTSTRAP_ADMIN_PASSWORD=${ADMIN_PASSWORD}") pairs+=("NETX_BOOTSTRAP_ADMIN_PASSWORD=${ADMIN_PASSWORD}")
fi fi
if ! is_loopback "${BIND}" && [[ -z "$(env_get "${ENV_FILE}" NETX_UME_VERIFY_TLS)" ]]; then # Deployments are on internal networks where UME uses self-signed certificates:
pairs+=("NETX_UME_VERIFY_TLS=true") # TLS verification is off by default and that check is waived explicitly
fi # (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 if [[ -n "${CRED_KEY}" ]]; then
pairs+=("NETX_CREDENTIAL_SECRET_KEY=${CRED_KEY}") pairs+=("NETX_CREDENTIAL_SECRET_KEY=${CRED_KEY}")
elif [[ -z "$(env_get "${ENV_FILE}" NETX_CREDENTIAL_SECRET_KEY)" ]]; then elif [[ -z "$(env_get "${ENV_FILE}" NETX_CREDENTIAL_SECRET_KEY)" ]]; then
@ -507,10 +510,7 @@ summary() {
echo echo
ok "NetX ${VERSION} $(t "已就绪" "is up")" ok "NetX ${VERSION} $(t "已就绪" "is up")"
echo " $(t "访问地址" "URL") : http://${host}:${port}/" echo " $(t "访问地址" "URL") : http://${host}:${port}/"
if [[ "${MODE}" == "install" && -n "${ADMIN_PASSWORD}" ]]; then [[ "${MODE}" == "update" ]] || print_admin_login "${ENV_FILE}"
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
echo " $(t "程序目录" "program") : ${PREFIX}/current -> releases/${VERSION}" echo " $(t "程序目录" "program") : ${PREFIX}/current -> releases/${VERSION}"
echo " $(t "数据目录" "data") : ${DATA}" echo " $(t "数据目录" "data") : ${DATA}"
echo " $(t "管理命令" "manage") : sudo netxctl status | stop | start | set-port | set-pg-port | update" 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 [[ "${AUTO_UPDATE}" == "1" ]]; then "${PREFIX}/current/packaging/linux/netxctl" auto-update on || true; fi
if ! start_and_wait; then if ! start_and_wait; then
err "$(t "NetX 启动失败,日志" "NetX failed to start; logs"): ${DATA}/logs/" 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}" show_start_failure "${DATA}"
exit 1 exit 1
fi fi
@ -578,6 +579,9 @@ do_update() {
# Re-assert keys newer releases rely on; never touches ports or DB settings. # 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 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}") 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[@]}" netx_admin env-set "${ENV_FILE}" "${pairs[@]}"
chown "${NETX_USER}:${NETX_USER}" "${ENV_FILE}" chown "${NETX_USER}:${NETX_USER}" "${ENV_FILE}"
switch_current "${VERSION}" switch_current "${VERSION}"

View file

@ -172,6 +172,29 @@ show_start_failure() {
fi 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; } is_loopback() { case "$1" in 127.0.0.1|localhost|::1) return 0 ;; *) return 1 ;; esac; }
primary_ip() { primary_ip() {

View file

@ -611,6 +611,37 @@ def cmd_tar_list(a: argparse.Namespace) -> None:
print(name) 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: def cmd_check_update(a: argparse.Namespace) -> None:
cfg = Cfg(read_env(Path(a.env))) cfg = Cfg(read_env(Path(a.env)))
cur = current_version(Path(a.prefix)) 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 = 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.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 = sub.add_parser("run-as"); s.add_argument("user"); s.add_argument("argv", nargs=argparse.REMAINDER)
s.set_defaults(fn=cmd_run_as) s.set_defaults(fn=cmd_run_as)
s = sub.add_parser("spawn"); s.add_argument("log"); s.add_argument("pidfile") s = sub.add_parser("spawn"); s.add_argument("log"); s.add_argument("pidfile")

View file

@ -43,6 +43,7 @@ $(t "用法" "Usage"): netxctl <command> [options]
update [--yes] [--file <netx-x.y.z-linux-x86_64.tar.gz>] [--force] update [--yes] [--file <netx-x.y.z-linux-x86_64.tar.gz>] [--force]
auto-update on|off $(t "每日自动更新" "daily automatic update") auto-update on|off $(t "每日自动更新" "daily automatic update")
backup [<file.tar.gz>] $(t "备份数据库与密钥" "back up database + secrets") backup [<file.tar.gz>] $(t "备份数据库与密钥" "back up database + secrets")
admin-password $(t "查看管理员登录密码情况" "show the admin login (initial password or not)")
uninstall [--purge] [--yes] uninstall [--purge] [--yes]
version version
run $(t "前台运行(systemd 使用)" "foreground supervisor (used by systemd)") run $(t "前台运行(systemd 使用)" "foreground supervisor (used by systemd)")
@ -235,6 +236,11 @@ cmd_logs() {
cmd_version() { cur_version; } cmd_version() { cur_version; }
cmd_admin_password() {
need_root admin-password; need_install
print_admin_login "${ENV_FILE}"
}
# ------------------------------------------------------------------ ports # ------------------------------------------------------------------ ports
# apply_env_change <description> KEY=VAL... ; restarts if running, rolls back on failure. # apply_env_change <description> KEY=VAL... ; restarts if running, rolls back on failure.
@ -295,7 +301,8 @@ cmd_set_bind() {
if ! is_loopback "${h}"; then if ! is_loopback "${h}"; then
[[ -n "$(cfg NETX_BOOTSTRAP_ADMIN_PASSWORD)" && "$(cfg NETX_BOOTSTRAP_ADMIN_PASSWORD)" != "admin123" ]] || [[ -n "$(cfg NETX_BOOTSTRAP_ADMIN_PASSWORD)" && "$(cfg NETX_BOOTSTRAP_ADMIN_PASSWORD)" != "admin123" ]] ||
pairs+=("NETX_BOOTSTRAP_ADMIN_PASSWORD=$(netx_admin gen-secret password)") 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 fi
apply_env_change set_bind "${pairs[@]}" apply_env_change set_bind "${pairs[@]}"
ok "$(t "监听地址已改为" "listen address is now") ${h}:$(api_port)" ok "$(t "监听地址已改为" "listen address is now") ${h}:$(api_port)"
@ -518,6 +525,7 @@ case "${cmd}" in
backup) cmd_backup "$@" ;; backup) cmd_backup "$@" ;;
uninstall) cmd_uninstall "$@" ;; uninstall) cmd_uninstall "$@" ;;
version|--version|-V) cmd_version ;; version|--version|-V) cmd_version ;;
admin-password) cmd_admin_password ;;
""|-h|--help|help) usage ;; ""|-h|--help|help) usage ;;
*) err "unknown command: ${cmd}"; usage; exit 2 ;; *) err "unknown command: ${cmd}"; usage; exit 2 ;;
esac esac

View file

@ -56,6 +56,8 @@ listening "${PG1}" || fail "PG not on ${PG1}"
token="$(login "${PORT1}")" || fail "login" token="$(login "${PORT1}")" || fail "login"
[[ -n "${token}" ]] || fail "empty token" [[ -n "${token}" ]] || fail "empty token"
netxctl status || fail "status" 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" step "marker row survives update"
PGPASSWORD="$(grep '^NETX_DATABASE_URL=' /var/lib/netx/.env | sed 's#.*://netx:\([^@]*\)@.*#\1#')" \ PGPASSWORD="$(grep '^NETX_DATABASE_URL=' /var/lib/netx/.env | sed 's#.*://netx:\([^@]*\)@.*#\1#')" \

View file

@ -63,6 +63,11 @@ $values = @{}
$values["NETX_DB_MODE"] = $DbMode $values["NETX_DB_MODE"] = $DbMode
$values["NETX_HOST"] = "127.0.0.1" $values["NETX_HOST"] = "127.0.0.1"
$values["NETX_PORT"] = "8890" $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. # Absolute UI path when present; else relative for source trees.
$distAbs = Join-Path $prog "web\dist" $distAbs = Join-Path $prog "web\dist"
if (Test-Path (Join-Path $distAbs "index.html")) { if (Test-Path (Join-Path $distAbs "index.html")) {