mirror of
https://github.com/hansjone/netx.git
synced 2026-10-15 06:03:18 +08:00
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:
parent
b0b64ba07d
commit
eb0eccdffe
7 changed files with 95 additions and 21 deletions
|
|
@ -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`。
|
||||||
|
|
||||||
| 路径 | 内容 |
|
| 路径 | 内容 |
|
||||||
|---|---|
|
|---|---|
|
||||||
|
|
|
||||||
|
|
@ -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}"
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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#')" \
|
||||||
|
|
|
||||||
|
|
@ -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")) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue