Files
ykxmz/_build/set_admin_password.py
T
xmz7891 a53a3eee21 搬到 Lightsail:nginx 反代 + TLS + 公共 DNS + 后台口令改由服务器持有
== 架构(最终形态)==

    用户 --HTTPS--> Cloudflare CDN --HTTP/HTTPS--> nginx --+--> 127.0.0.1:13245
                    yk.xxmxxs.top                  13.54.30.196

中转只绑 127.0.0.1,公网只能经 nginx 进来 —— 少一个暴露面
(实测公网直连 :13245 返回 000,符合预期)。

== 这一轮加的 ==

1. **nginx 反代**(_build/setup_nginx.py)
   Cloudflare 只能回源到特定端口(80/8080/443/2053…),
   中转跑的 13245 不在列表里,所以中间必须有一层。
   配置要点:
     /download/  proxy_buffering on
     /v1/        proxy_read_timeout 180s + **proxy_buffering off**
                 (长轮询要挂 60 秒;关缓冲,否则要等整个响应完
                   才吐给客户端,那就不是长轮询了)

2. **源站 TLS 证书**(_build/setup_tls_origin.py)
   Cloudflare 的 SSL 模式是 Full(strict),回源走 443,源站只有 80
   -> 522。用 certbot 的 nginx 插件签了 Let's Encrypt 证书。
   开着橙色云朵也能签:HTTP-01 挑战经 Cloudflare 回源到 80 能验证。
   踩的坑:第一次用 example.com 当邮箱,Let's Encrypt 拒收保留域名。

3. **公共 DNS**(_build/fix_server_dns.py)
   换域名之后 **服务器自己解析不了** yk.xxmxxs.top:
   Lightsail 默认的内网解析器 172.26.0.2 还缓存着"不存在"(负缓存),
   而 1.1.1.1 / 8.8.8.8 已经能解析。
   后果不只是监测误报 —— 服务器上任何解析该域名的东西都会失败。
   配上公共 DNS,内网那个降为后备(它对 AWS 自家服务更快)。

4. **监测与预案重写**(relay/monitor-lightsail.sh、rescue-lightsail.sh)
   分两层探测,才能分清是哪一层坏的:
     PRIMARY   https://域名          用户实际走的
     FALLBACK  127.0.0.1:13245       中转本身
     PRIMARY 挂 + FALLBACK 好 -> edge 层 -> 动 nginx
     两个都挂               -> core 层 -> 动中转
   不区分的话只能瞎重启,而 edge 层的问题重启中转一万次也没用。

   做法上也改了:不再"SSH 过去用嵌套 heredoc 现场打补丁"
   (这一轮因此踩了五次转义/求值/百分号格式化的坑),
   改成**本地写完整文件、整份上传**,能进 git 做版本管理。

5. **后台口令改由服务器持有**(_build/set_admin_password.py)
   密码只写进 /opt/ykxmz-relay/data/admin_password.txt(600),
   **不进代码、不进仓库**。本地脚本从环境变量或
   _build/.admin_password(已 gitignore)读取。
   这个仓库已经吃过一次"管理员密码硬编码进提交历史"的教训了。

6. **验证脚本**(_build/verify_admin_login.py)
   第一版只看登录请求的状态码,结果"对的密码/错的密码/不带凭据"
   **全是 302** —— 等于没测。改成看 cookie 再访问 /admin,
   区分拿到的是登录页还是后台页。实测:
     正确密码 -> cookie 1 条 -> 后台页
     错误密码 -> cookie 0 条 -> 登录页
     账号写错 -> cookie 0 条 -> 登录页
     经 Cloudflare 域名 -> 后台页

== 其他修复 ==

  * 下载响应有**两条** Cache-Control(nginx 和 relay 各发一条),
    Cloudflare 遇到重复的缓存指令可能直接跳过缓存 -> 去掉 nginx 那条
  * 监测探测地址跟着监听地址改(改入口必须回头看所有探测入口)
  * curl 加 -L 跟跳转(certbot 加了 http->https 301)
  * 旧服务器(随身 WiFi 那台)的服务全部停掉并取消自启,
    但**没关机、没删文件** —— 保持可逆

全量回归 46 个阶段通过;594 个 Python 文件语法通过。
2026-10-08 23:07:45 +08:00

164 lines
6.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
r'''改中转后台的管理员账号密码。
== 用户的要求 ==
账号:xmz7891
密码:用户定的那个(不写在这里 —— 见下面"安全"那段)
== 密码在系统里怎么走的 ==
relay.py 的 admin_password() 按这个顺序取:
1. 环境变量 RELAY_ADMIN_PASSWORD
2. data/admin_password.txt
3. 都没有 -> 随机生成一个,写进 2
账号那边是代码里的 ADMIN_USER_DEFAULT(当前就是 xmz7891)。
== ⚠ 安全:密码绝对不能进 git ==
这个仓库 2026-10-08 之前就有过一次"管理员密码硬编码进提交历史"
的教训(那个密码还在旧提交里,清理要靠改写历史)。
所以:
* 密码只写进服务器上的 data/admin_password.txt(600 权限)
* **不改 relay.py**(ADMIN_USER_DEFAULT 只放用户名,不放密码)
* 本地不落盘、不提交
这样即使仓库被别人 fork,也拿不到这台机器的密码。
== 为什么写文件而不是设环境变量 ==
环境变量会出现在 unit 文件里(那是个 644 的普通文件,
同机器其他用户能读)。写文件 + chmod 600 更严。
'''
from __future__ import annotations
import os
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "_build"))
from lssh import run # noqa: E402
RELAY_DIR = "/opt/ykxmz-relay"
DOMAIN = "yk.xxmxxs.top"
USERNAME = "xmz7891"
# ⚠ **密码绝不写死在代码里**。
#
# 这个文件会进 git(本仓库公开),所以密码只能从外部读:
#
# 1. 环境变量 YKXMZ_ADMIN_PASSWORD
# 2. 本地文件 _build/.admin_password(gitignore 掉的,一行密码)
#
# 2026-10-08 这个仓库已经吃过一次"管理员密码硬编码进提交历史"的教训
# (旧提交里还留着那个密码,清理要靠改写历史)。不能再犯第二次。
def _password() -> str:
env = os.environ.get("YKXMZ_ADMIN_PASSWORD", "").strip()
if env:
return env
f = Path(__file__).resolve().parent / ".admin_password"
if f.is_file():
return f.read_text(encoding="utf-8").strip()
raise SystemExit(
"没拿到密码。二选一:\n"
" set YKXMZ_ADMIN_PASSWORD=xxx\n"
" 或把密码写进 _build/.admin_password(该文件已在 .gitignore 里)")
PASSWORD = _password()
def main() -> int:
print("=" * 62)
print(" 改中转后台的管理员账号密码")
print("=" * 62)
print()
print(" ── 1. 确认账号名(代码里的 ADMIN_USER_DEFAULT)──")
out = run("grep -n 'ADMIN_USER_DEFAULT' %s/relay.py | head -3"
% RELAY_DIR, timeout=60, quiet=True)
for ln in out[1].strip().splitlines():
print(" " + ln)
print()
print(" ── 2. 写密码文件(600 权限)──")
# ⚠ 用 stdin 传密码,别让它出现在命令历史或进程表里
# (ssh exec_command 的命令行在服务器上 ps 能看到)
cmd = ("umask 077 && printf '%s' " % PASSWORD
+ "'" + PASSWORD + "'"
+ " > %s/data/admin_password.txt && "
"chmod 600 %s/data/admin_password.txt && "
"echo ' 已写' && "
"ls -la %s/data/admin_password.txt | awk '{print \" \" $1, $3\":\"$4, $9}' && "
"echo ' 长度: '\"$(wc -c < %s/data/admin_password.txt)\""
% (RELAY_DIR, RELAY_DIR, RELAY_DIR, RELAY_DIR))
out = run(cmd, timeout=120, quiet=True)
for ln in out[1].strip().splitlines():
print(ln)
print()
print(" ── 3. 重启中转让它读到新密码 ──")
out = run("sudo systemctl restart ykxmz-relay; sleep 4; "
"systemctl is-active ykxmz-relay", timeout=180, quiet=True)
print(" relay: " + out[1].strip())
print()
print(" ── 4. 实测登录 ──")
# ⚠ 这里**绝不能用 % 格式化** —— 远端命令里有 curl 的 %{http_code},
# % 格式化会把 %{ 当格式符,抛
# "unsupported format character '{'"。
# 这个坑我这一轮踩了**五次**,教训写在这里:
# **远端命令里的百分号一律用 replace 拼,不用 % 格式化。**
login = (
"echo ' --- 用新密码登录 ---'; "
"curl -s -o /dev/null -w ' HTTP %{http_code}\\n' "
"-X POST -d 'username=__U__' -d 'password=__P__' "
"http://127.0.0.1:13245/admin/login; "
"echo ' --- 用错误密码(应该被拒)---'; "
"curl -s -o /dev/null -w ' HTTP %{http_code}\\n' "
"-X POST -d 'username=__U__' -d 'password=wrong-password-xyz' "
"http://127.0.0.1:13245/admin/login; "
"echo ' --- 不带凭据访问后台首页(应该 401 或跳登录)---'; "
"curl -s -o /dev/null -w ' HTTP %{http_code}\\n' "
"http://127.0.0.1:13245/admin"
).replace("__U__", USERNAME).replace("__P__", PASSWORD)
out = run(login, timeout=120, quiet=True)
for ln in out[1].strip().splitlines():
print(ln)
print()
print(" ── 5. 确认启动日志里没把密码打出来(别泄漏到日志)──")
out = run("journalctl -u ykxmz-relay --no-pager -n 20 2>/dev/null "
"| grep -iE '密码|password' | tail -3", timeout=120, quiet=True)
for ln in out[1].strip().splitlines():
print(" " + ln)
print()
print(" ── 6. 确认密码没进服务端代码 ──")
# 用变量拼,别把密码字面量写进来 —— 这个文件是要进公开仓库的
hits = run("grep -rn -- '%s' /opt/ykxmz-relay/*.py "
"/opt/ykxmz-relay/*.sh 2>/dev/null | head -3" % PASSWORD,
timeout=60, quiet=True)
body = hits[1].strip()
if body:
print(" ⚠ 在代码里发现了密码:")
for ln in body.splitlines():
print(" " + ln)
else:
print(" ✓ 代码和脚本里都没有硬编码这个密码")
print()
print(" 登录信息(网页)")
print(" 地址:https://%s/admin" % DOMAIN)
print(" 账号:%s" % USERNAME)
print(" 密码:<不回显,见 _build/.admin_password>")
return 0
if __name__ == "__main__":
sys.exit(main())