主题
07 — 变更记录
按时间倒序记录所有对服务器的变更。每次操作后请追加一行。
变更一览
| 时间 (CST) | 操作人 | 变更内容 | 回滚方式 |
|---|---|---|---|
| 2026-11-08 09:35 | DSH | TenoDX Hub 打包为 exe:新增 tools/TenoDXHub/build.ps1(PyInstaller onefile,9.28 MB);hub.py 增加 resource_dir()/data_dir() 以适配单文件 exe(UI 读 _MEIPASS,缓存/配置写 exe 同级) | 删除 build.ps1 与 dist/ |
| 2026-11-08 09:35 | DSH | 实机验证:exe 在 COM25 正确识别 H503 控制器(USB 序列 3514343932345103,Magic 能力 0x01 0x02) | — |
| 2026-11-08 05:40 | DSH | 部署 VitePress 文档站 tenodx-docs(构建进镜像 + 容器内 Caddy 托管,23 篇 md);挂到 Caddy docs.localhost;纳入监控(第 12 项) | cd /opt/stacks/docs && docker compose down;恢复 Caddyfile.bak.* |
| 2026-11-08 05:33 | DSH | 新增 docs-site/(VitePress 配置 + prepare.ps1 内容装配 + Dockerfile/compose) | 删除该目录 |
| 2026-11-08 05:32 | DSH | 新增 tools/TenoDXHub/(H503 本地 Hub:控制器信息 + 固件拉取刷入) | 删除该目录 |
| 2026-11-08 05:30 | DSH | 固件资源服务器挂到 Caddy(fw.localhost)并纳入监控(第 11 项);tunnel.ps1 新增 fw 服务 | 恢复 Caddyfile.bak.* |
| 2026-11-08 05:28 | DSH | 部署固件资源服务器 TenoDX Firmware Registry 1.0.0(API-only,FastAPI + 库 tenodx_fw + 卷 tenodx-fwdata) | cd /opt/stacks/fw-registry && docker compose down -v;drop database tenodx_fw |
| 2026-11-08 05:25 | DSH | Caddy 纳入监控(第 10 项);tunnel.ps1 新增 web 服务并修复逗号参数解析 | — |
| 2026-11-08 05:22 | DSH | 修复 Caddy basic auth 恒 401:bcrypt 哈希被 compose $ 插值破坏,改为内联进 Caddyfile | 恢复 compose.yaml 环境变量写法(会复现故障) |
| 2026-11-08 05:18 | DSH | 部署 Caddy v2.11.9 作为 loopback Web 入口:Host 路由 + basic auth + 落地页 + /healthz | cd /opt/stacks/caddy && docker compose down -v |
| 2026-11-08 05:13 | DSH | 新增 backup-stacks.sh + cron(每日 03:35 备份栈配置 / kuma.db / NapCat 配置) | rm /etc/cron.d/tenodx-stack-backup |
| 2026-11-08 05:11 | DSH | Uptime Kuma 9 项监控全部就位(8 启用 + 1 待 QQ 登录),含宿主 cron 推送磁盘/代理 | 见 11-应用服务.md |
| 2026-11-08 05:04 | DSH | 配置 NapCat OneBot11 HTTP(3000)/WS(3002),发布端口(仅 loopback) | 改回 compose 端口映射 |
| 2026-11-08 05:02 | DSH | 部署 NapCat 容器(mlikiowa/napcat-docker,privileged,2.16 GB 镜像) | cd /opt/stacks/napcat && docker compose down -v |
| 2026-11-08 05:00 | DSH | 部署 Uptime Kuma 2.5.9(SQLite,跳过 2.x 向导) | cd /opt/stacks/uptime-kuma && docker compose down -v |
| 2026-11-08 04:33 | DSH | 新增 scripts/tunnel.ps1(本机 SSH 隧道助手,绕过安全组访问 loopback 服务) | 删除该文件 |
| 2026-11-08 04:32 | DSH | PostgreSQL 备份恢复演练通过:1000 行 → 备份 → 删表 → 恢复到临时库 → 1000 行完整还原;docker 守护进程重启后容器自动恢复 | 无变更(演练后已清理临时库与测试表) |
| 2026-11-08 04:30 | DSH | 外部端口探测(73 端口):确认安全组仅放行 22/tcp + ICMP,80/443 被阻断,Web 服务当前无法对外 | 无变更(只读探测) |
| 2026-11-08 04:22 | DSH | 部署 PostgreSQL 18 容器(tenodx-postgres),数据卷 tenodx-pgdata,仅绑 127.0.0.1:5432;建库 tenodx/用户 tenodx | cd /opt/stacks/postgres && docker compose down -v |
| 2026-11-08 04:22 | DSH | 新增 /opt/stacks/postgres/backup.sh + /etc/cron.d/tenodx-postgres-backup(每日 03:30,保留 7 天) | rm /etc/cron.d/tenodx-postgres-backup |
| 2026-11-08 04:22 | DSH | effective_cache_size 调为 768MB(默认按更大内存估算,对本机偏高) | ALTER SYSTEM RESET effective_cache_size; |
| 2026-11-08 04:21 | DSH | 拉取 postgres:18-alpine 镜像 | docker rmi postgres:18-alpine |
| 2026-11-08 04:20 | DSH | 立规:新增 10-部署规范.md —— 软件一律 Docker 运行(除非必裸装) | 删除该文档 |
| 2026-11-08 04:17 | DSH | 安装 fail2ban 0.11.2(sshd jail,白名单运维 IP);添加 2 GB swapfile 并写入 fstab;vm.swappiness 0→10 | apt purge fail2ban;swapoff /swapfile && rm /swapfile + 删 fstab 行 |
| 2026-11-08 04:16 | DSH | 关闭 SSH 密码登录 PasswordAuthentication no + X11Forwarding no,已 reload 并验证密钥登录正常 | 恢复 /root/sshd-99-keepalive.backup-20261108-041557.conf 后 systemctl reload sshd |
| 2026-11-08 04:16 | DSH | 新增 09-决策记录.md,记录全部自主决策与理由 | — |
| 2026-11-08 04:12 | DSH | 更新档案:新增 08-网络代理.md,刷新 02/03/05 与 inventory.yaml | — |
| 2026-11-08 04:12 | DSH | 安装 /usr/local/bin/px 代理使用助手 | 删除该文件 |
| 2026-11-08 04:11 | DSH | 安装 Docker 29.9.3 + Compose v5.7.0;清理 daemon.json 失效镜像源(9→6 个);restart docker 应用配置 | apt purge docker-ce* containerd.io + 恢复 daemon.json.bak.* |
| 2026-11-08 04:10 | DSH | 系统升级:apt update/upgrade/dist-upgrade/autoremove,升级 18 个包(cloud-init、intel-microcode 因被 hold 未升级) | 快照回滚 |
| 2026-11-08 04:08 | DSH | 安装 shadowsocks-libev + privoxy,自建 ss-local-us01.service(SOCKS5 127.0.0.1:1080);privoxy 转 HTTP 代理 127.0.0.1:8118 | systemctl disable --now ss-local-us01 privoxy |
| 2026-11-08 04:05 | DSH | 启用 ufw 防火墙:放行 22/80/443;DEFAULT_FORWARD_POLICY DROP → ACCEPT(供 Docker) | ufw disable;恢复 /etc/default/ufw.bak.* |
| 2026-11-08 04:00 | DSH | 建立服务器信息档案 server-info/ | 删除目录 |
| 2026-11-08 03:58 | DSH | 本机 ~/.ssh/config 写入保活配置 + tenodx 别名 | 恢复 config.bak.20261108-035615 |
| 2026-11-08 03:57 | DSH | 本机 known_hosts 写入服务器 3 个主机公钥 | 删除对应行 |
| 2026-11-08 03:56 | DSH | 服务端新建 /etc/ssh/sshd_config.d/99-keepalive.conf(ClientAliveInterval 60 / CountMax 10),sshd -t 校验后 reload | 删除该文件后 systemctl reload sshd |
| 2026-11-08 03:56 | DSH | 生成 ED25519 密钥,公钥写入服务器 /root/.ssh/authorized_keys,实现免密登录 | 移除 authorized_keys 中对应行 |
| 2026-10-30 10:23 | 人工 | sudo passwd root 设置 root 密码 | — |
| 2026-10-30 10:04 | 人工 | sed 将 PasswordAuthentication 改为 yes,重启 sshd | 改回 no |
| 2026-10-30 10:07 | 系统 | 重启,内核 5.15.0-191-generic | — |
| 2026-09-16 14:22 | 系统 | 系统初始化 / 首次可用(镜像 ubuntu_22_04_x64_20G_alibase_20260916.vhd) | — |
详细记录
2026-11-08 05:26~05:32 — 固件资源服务器 + H503 Hub
背景:需要给 H503 项目发固件;服务端要纯 API(无浏览器 UI)且上传带鉴权; 本地要一个轻量、UI 现代、左侧列表的 Hub,能拉版本、看时间和更新说明、下载并刷入。
1. 选型决策
否决 MinIO / Gitea / Nexus / Artifactory(都自带 Web UI,且 200~500 MB 内存起步, 本机只剩 ~1 GB)。自建 FastAPI:元数据存进已有的 PostgreSQL(新库 tenodx_fw), 这样"每日 pg_dump 备份"和"数据库监控"自动覆盖新服务,不新增有状态组件。
2. 服务端实现
server/fw-registry/(Dockerfile + app/main.py + publish.ps1)
| 端点 | 鉴权 |
|---|---|
GET /api/v1/health | 无 |
GET /api/v1/firmware / /latest / /{id} / /{id}/download | 无(下载鉴权留了开关) |
POST /api/v1/firmware | X-API-Token = FW_ADMIN_TOKEN |
PATCH / DELETE /api/v1/firmware/{id} | 同上 |
设计要点:
- 显式关闭
docs_url/redoc_url/openapi_url—— 满足"不要浏览器 UI" - 版本号用智能比较(
1.10 > 1.9),不是按上传时间排序 - 上传边写边算 SHA256,落盘先写临时文件再原子 rename
- 扩展名白名单 + 文件名清洗,防目录穿越
- 401 比较用
hmac.compare_digest
3. 验证(全部通过)
无 token 上传 401 / 错误 token 401 / 无 token 删除 401
上传 201 + sha256;下载 SHA256 与上传完全一致
/docs /openapi.json / 均为 404(确认 API-only)
Caddy Host: fw.localhost -> health 200, list 正常4. Hub 实现
tools/TenoDXHub/(hub.py + static/index.html + run.ps1)
为什么不用 Electron / 前端框架:需求是"轻量"。后端只用标准库 http.server,唯一第三方依赖 pyserial(现有 DFU 工具已需要);UI 是单文件 HTML+CSS+JS,无构建步骤。整个程序不打包时约 30 KB 源码。
刷写逻辑移植自 tools/h503_dfu_flasher:Magic 0x84/0xA5 进 DFU → 等 0483:DF11 → STM32_Programmer_CLI -c port=USB1 -w ... -v -rst → 认 Download verified successfully。
5. 端到端联调
隧道 18000→8000 → publish.ps1/curl 上传 9.9.9 → Hub /api/versions 返回 v9.9.9 [stable] 4KB + 三条更新说明 → 删除 → 列表清空。全链路通。
6. 阻塞项(重要)
安全组只放行 22/tcp,固件服务公网不可达。 当前只能走 SSH 隧道。要真正向用户分发,必须先放行 80/443 (02-防火墙与端口.md 第 7 节)+ 配域名(12-Web入口.md 第 7 节)。
7. 回滚
bash
cd /opt/stacks/fw-registry && docker compose down -v
docker exec tenodx-postgres psql -U tenodx -d postgres -c 'DROP DATABASE tenodx_fw;'
cd /opt/stacks/caddy && cp Caddyfile.bak.<最新> Caddyfile && docker compose restart caddy
rm -rf /opt/stacks/fw-registry2026-11-08 05:18~05:25 — 部署 Caddy(Web 入口 / 反向代理)
背景:用户要求安装 Caddy 并配置基本信息。
1. 先验证了一个决定架构的事实
部署前实测:两个应用都不支持子路径。
| 应用 | 结论 | 依据 |
|---|---|---|
| Uptime Kuma 2.5.9 | 无 base-path 支持 | 镜像中不存在任何 BASE_URL 类环境变量 |
| NapCat WebUI | 绝对路径资源 | HTML 里是 /webui/assets/index-*.js |
→ 只能做 Host 头虚拟主机,路径式反代会坏。如果照抄"nginx 反代 /uptime/"的教程,直接就是坏的。
2. 镜像版本坑
caddy:2-alpine 从镜像站拉到的是 v2.4.6(2021 年),缓存过期。 查 tag 列表后改用 caddy:2.11.9-alpine(当前版本,且 basic_auth 指令是 2.8+ 才有)。
3. 部署
/opt/stacks/caddy/
├── compose.yaml caddy:2.11.9-alpine, 127.0.0.1:80
├── Caddyfile Host 路由 + basic auth + /healthz
├── site/index.html 落地页(自动适配隧道端口)
├── logs/access.log 20MiB × 5 轮转
└── .env basic auth 凭据 (600)关键设计:
/healthz放在 basic_auth 之前,否则监控拿到 401 会误判宕机auto_https off(无域名 + 安全组封 80/443,ACME 不可能成功),配置里留好将来切换的注释块- 端口只绑
127.0.0.1:80
4. ⚠️ 踩坑:bcrypt 哈希被 Docker Compose 的 $ 插值破坏
现象:带正确密码访问 kuma.localhost 仍返回 401。
原因:bcrypt 哈希形如 $2a$14$AbC...,而 Docker Compose 会对 environment: 里的值做 $ 变量插值,把每段 $xxx 当变量名替换成空串。 日志铁证:
The "j6giqe3" variable is not set. Defaulting to a blank string.(j6giqe3 就是哈希里被误认成变量名的一段)
修复:哈希内联进 Caddyfile。Caddy 只解析 {$VAR} 语法,裸 $2a$14$... 是字面量。 写入时用带引号的 heredoc + 占位符 + sed,避免 shell 展开 $。
修复后验证:
| 请求 | 结果 |
|---|---|
kuma.localhost 无凭据 | 401 ✅ |
kuma.localhost 有凭据 / | 302 → /dashboard(Kuma 自身行为) |
kuma.localhost 有凭据 /dashboard | 200,<title>Uptime Kuma |
napcat.localhost 有凭据 /webui/ | 200,<title>NapCat WebUI |
/healthz | 200 ok |
/favicon.ico(穿透静态资源) | 200 |
5. 回滚
bash
cd /opt/stacks/caddy && docker compose down -v
rm -rf /opt/stacks/caddy2026-11-08 05:00~05:15 — NapCat + Uptime Kuma 部署
背景:用户要求用 Docker 安装 NapCat 与 uptime(监控),并要求 "uptime 应该及时追加新服务的检测,所有服务都应该被监测,由你来添加,加入到定期维护内"。
1. Uptime Kuma 2.5.9
bash
/opt/stacks/uptime-kuma/compose.yaml # louislam/uptime-kuma:2
UPTIME_KUMA_DB_TYPE=sqlite # 跳过 2.x 的"选择数据库"向导
extra_hosts: host.docker.internal:host-gateway
ports: 127.0.0.1:3001踩坑 3 次:
- 2.x 首次启动停在
Waiting for user action...(数据库选择向导,无法无头完成) → 用UPTIME_KUMA_DB_TYPE=sqlite跳过。 - 设了环境变量后仍报
no such table: setting→ 残留的 0 字节kuma.db被误判为"已有库",迁移没执行。删掉空文件后正常建表。 api-router.js只有 badge 接口,没有 monitor CRUD → 只能直接写 SQLite,于是做成monitors.json+sync.py的声明式方案。
2. 声明式监控同步(满足"由你添加、纳入维护"的要求)
/opt/stacks/uptime-kuma/
├── monitors.json ← 唯一事实源(9 条)
├── sync.py ← 幂等 upsert(新增/更新,不删除)
├── sync.sh ← sync + 重启容器
└── host-checks.sh ← 宿主 cron 每 5 分钟推送 3 项新增监控 = 改 monitors.json 一行 + 跑 sync.sh,不再需要点界面。
3. NapCat
bash
/opt/stacks/napcat/compose.yaml # mlikiowa/napcat-docker:latest
privileged: true # 镜像要求,见下
ports: 127.0.0.1:6099 / 3000 / 3002
mem_limit: 1024m镜像行为:entrypoint 会 mount --bind 伪造 cgroup / mountinfo / /proc/1/cmdline / sched / status / /etc/hosts / DMI,并删除 /.dockerenv, 目的是让 QQ 认为运行在真机上。这些操作需要 CAP_SYS_ADMIN,所以必须 privileged。
已评估为已知高权限风险:privileged 容器≈宿主 root,而 QQ 是闭源二进制。 缓解:不挂 docker.sock、不用 host 网络、
mem_limit: 1g、日志轮转。 详细风险说明见11-应用服务.md第 2 节。
验证:容器 running,日志出现 [PacketHandler] 加载成功 / [Napi2NativeLoader] 加载成功,QQ 进程正常, WebUI http://127.0.0.1:6099/webui/ 返回 200。
WebUI 实际路径是 /webui/(根路径 301 跳转过去,/napcat/ 是 404)。
⚠️ QQ 尚未登录:需要你本人扫码。容器已就绪,登录流程见 11-应用服务.md。
4. 监控踩坑(4 个,都会让监控静默失效)
| # | 坑 | 现象 | 正解 |
|---|---|---|---|
| 1 | TCP 类型名是 port 不是 tcp | Unknown Monitor Type,看起来像服务挂了 | 数据库 type='port' |
| 2 | 容器内 127.0.0.1 是自己 | ECONNREFUSED 127.0.0.1:5432 | 用容器名 tenodx-postgres:5432 |
| 3 | docker-proxy 先接受 TCP 握手 | 对发布端口用 port 类型永远显示 UP(假阳性) | 改用 http 类型 |
| 4 | push 的 msg 含空格 | curl 失败 http 000,推送静默丢失 | msg 用下划线 port_1080_ok |
5. 最终监控状态(全部 UP)
SSH 22 UP 2 ms
PostgreSQL 18 UP
Uptime Kuma UP 200 - OK
NapCat WebUI UP 200 - OK
NapCat OneBot HTTP (暂停,待 QQ 登录)
Outbound Internet UP 200 - OK
SOCKS5 proxy UP (push, port_1080_ok)
HTTP proxy (privoxy) UP (push, port_8118_ok)
Disk / usage UP (push, root_31pct)6. 回滚
bash
cd /opt/stacks/napcat && docker compose down -v
cd /opt/stacks/uptime-kuma && docker compose down -v
rm -f /etc/cron.d/tenodx-monitor-host /etc/cron.d/tenodx-stack-backup
rm -rf /opt/stacks/napcat /opt/stacks/uptime-kuma /var/backups/stacks
docker rmi mlikiowa/napcat-docker:latest louislam/uptime-kuma:22026-11-08 04:32 — PostgreSQL 备份恢复演练(只读验证,无残留)
目的:确认备份不只是"gzip 能解开",而是真的能恢复数据。
流程:
写入 1000 行测试数据 → 执行 backup.sh → 备份内含 _drill 表
→ 模拟数据丢失(drop table) → 恢复到临时库 restore_test
→ 校验行数一致 → 抽查首行 → 清理临时库与测试表结果:
| 步骤 | 结果 |
|---|---|
备份前 tenodx._drill 行数 | 1000 |
| 备份文件 | tenodx-20261108-043238.sql.gz(8.0K) |
| gzip 完整性 | ✅ OK |
备份含 CREATE TABLE public._drill | ✅ 找到 1 处 |
| drop 后查询 | 如期报错 relation "_drill" does not exist |
恢复到 restore_test | ✅ 无 error/fatal |
| 恢复后行数 | 1000 → 与备份前完全一致 |
抽查 id=1 | row-1 ✅ |
| 清理后数据库列表 | postgres, tenodx(干净,无残留) |
结论:备份链路可用,灾难恢复有保障。
额外验证:容器重启韧性
bash
systemctl stop docker && systemctl start docker
# → tenodx-postgres 自动回到 running + healthy,数据库可查询证明 restart: unless-stopped 生效,宿主 docker 重启后数据库会自己回来。
已知未做:整机重启测试。虽然
findmnt --verify显示 fstab 0 错误、 所有服务均enabled,但在无人值守且无法确认 VNC 的情况下重启服务器, 一旦启动失败将长时间失联。这是刻意保守的决定,建议你在场时再做一次重启验证。
2026-11-08 04:20~04:22 — 立"容器优先"规范 + 部署 PostgreSQL 18
背景:用户要求「软件除非必应该使用 docker 运行」,然后安装 PostgreSQL。 按新规范,PostgreSQL 不用 apt 安装,而是容器化部署。
1. 立规(10-部署规范.md)
明确默认容器化,并列出允许裸装的 5 类例外(运行时本体 / 内核网络 / 防火墙 / 出口代理 / 系统基础组件)。
2. 关键发现:PostgreSQL 18 镜像改了数据目录
bash
$ docker image inspect postgres:18-alpine --format '{{json .Config.Volumes}} {{range .Config.Env}}{{println .}}{{end}}'
{"/var/lib/postgresql":{}}
PGDATA=/var/lib/postgresql/18/docker
postgres:18起PGDATA变为/var/lib/postgresql/18/docker, 声明卷是父目录/var/lib/postgresql。 若沿用老教程的/var/lib/postgresql/data挂载,数据会写到容器匿名卷里, 重建容器即丢数据。本次已按新约定挂载。
另一个坑:docker manifest inspect 走官方 registry(被墙),全部报不存在; 只有 docker pull 才会走 daemon.json 里配置的镜像加速。 判断镜像是否存在必须用 docker pull 实测。
3. compose 部署
yaml
# /opt/stacks/postgres/compose.yaml
image: postgres:18-alpine
container_name: tenodx-postgres
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB} # 来自 .env (600)
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C.UTF-8"
TZ: Asia/Shanghai
ports:
- "127.0.0.1:5432:5432" # 只绑 loopback,Docker 会绕过 ufw
volumes:
- pgdata:/var/lib/postgresql # ← 注意是父目录
shm_size: 256mb
stop_grace_period: 60s # 保证干净关闭,避免数据损坏
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]认证信息:POSTGRES_DB=tenodx、POSTGRES_USER=tenodx、密码随机 32 位存于 /opt/stacks/postgres/.env(权限 600,未落任何日志)。
4. 验证结果
| 验证项 | 结果 |
|---|---|
| 版本 | PostgreSQL 18.6 (x86_64-pc-linux-musl) |
| 健康检查 | ✅ healthy |
| 编码 / 排序规则 | UTF8 / datcollate=C / datctype=C.UTF-8 |
| 时区 | Asia/Shanghai(日志时间戳为 CST,tzdata 生效) |
| 持久化测试 | 建表→插数据→docker restart→数据仍在 ✅ |
| 宿主机端口绑定 | 127.0.0.1:5432 ✅ |
| 公网可达性 | 39.107.41.224:5432 不可达 ✅(对照组 22 可达) |
| 内存占用 | 26 MB(容器的 1.51%) |
| 逻辑备份 | ✅ 生成 tenodx-20261108-042237.sql.gz |
5. 备份
bash
# /opt/stacks/postgres/backup.sh → 每日 03:30 via /etc/cron.d/tenodx-postgres-backup
pg_dump --no-owner --no-privileges | gzip > /var/backups/postgres/tenodx-<ts>.sql.gz
# 校验: gzip -t + 非空;保留 7 天用逻辑备份而非文件拷贝:直接拷贝运行中的
PGDATA可能得到不一致的备份。
回滚(彻底移除):
bash
cd /opt/stacks/postgres && docker compose down -v # -v 会删数据卷
rm -f /etc/cron.d/tenodx-postgres-backup
rm -rf /opt/stacks/postgres /var/backups/postgres
docker rmi postgres:18-alpine2026-11-08 04:16~04:17 — 安全加固收尾(自主决策)
背景:用户授权"问题全部由我决定"后休息,我完成了剩余加固项。 完整决策理由见 09-决策记录.md。
1. 关闭 SSH 密码登录 + X11 转发
追加到 /etc/ssh/sshd_config.d/99-keepalive.conf:
PasswordAuthentication no
X11Forwarding no⚠️ 关键安全措施:这是唯一可能导致失联的操作,因此用了带回滚保险的两阶段流程:
bash
# 1) 挂 10 分钟后自动还原的 at 任务
echo "cp -a $BACKUP $DROPIN && systemctl reload sshd" | at now + 10 minutes
# 2) 应用变更 + sshd -t 校验 + reload
# 3) 从外部验证密钥登录
# 4) 验证通过后 atrm 取消回滚踩坑 #1(重要):第一版脚本里混入了中文字符串, 导致 bash 引号解析失败(syntax error near unexpected token '('), 配置写进了文件但后续的 reload 步骤被吞掉。
发现方式:验证时 ssh -o PubkeyAuthentication=no 返回 Permission denied (publickey,password) —— 仍列出 password, 说明运行中的 sshd 尚未重载(正确应只显示 publickey)。
修复:改用纯 ASCII 脚本重新执行 reload,之后返回 Permission denied (publickey)。
教训:远程 SSH 配置脚本必须纯 ASCII。 中文注释或输出都可能破坏 bash 引号配对。
验证结果:
| 测试 | 结果 |
|---|---|
ssh tenodx(别名,BatchMode) | ✅ 成功 |
ssh root@39.107.41.224(明文形式) | ✅ 成功 |
| 仅密码认证尝试 | ✅ Permission denied (publickey) — 密码已不可用 |
sshd -T | passwordauthentication no / x11forwarding no |
回滚备份:/root/sshd-99-keepalive.backup-20261108-041557.conf
2. fail2ban(04:17)
bash
apt-get install -y fail2ban # v0.11.2/etc/fail2ban/jail.local:
ini
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
ignoreip = 127.0.0.1/8 ::1 223.73.45.39
banaction = ufw
backend = systemd
[sshd]
enabled = true
port = 22
mode = normalbanaction = ufw—— 封禁规则直接写进 ufw,ufw status可见ignoreip白名单运维 IP,防止把自己封掉- 状态:
fail2ban-client status sshd→ 当前封禁 0 个
3. Swap(04:17)
bash
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
echo 'vm.swappiness=10' > /etc/sysctl.d/99-swap.conf- fstab 校验:
findmnt --verify→ 0 parse errors(1 个 warning 属于 swapfile 正常提示) mount -a无报错- 备份:
/etc/fstab.bak.20261108-* - 生效:
Swap: 2.0Gi,vm.swappiness = 10
回滚:
bash
swapoff /swapfile && rm /swapfile
sed -i '/^\/swapfile/d' /etc/fstab
systemctl disable --now fail2ban && apt-get purge -y fail2ban
sed -i '/^PasswordAuthentication no/d;/^X11Forwarding no/d' /etc/ssh/sshd_config.d/99-keepalive.conf
sshd -t && systemctl reload sshd2026-11-08 04:08~04:12 — 防火墙 / 系统升级 / Docker / 网络代理
背景:服务器为全新空机,需完成基础环境搭建:防火墙、系统补丁、容器运行时, 以及解决国内网络访问 GitHub Raw / Docker Hub 的问题。
执行顺序(刻意如此:先防火墙 → 再升级 → 再装 Docker → 最后代理):
1. ufw 防火墙(04:05)
先改 DEFAULT_FORWARD_POLICY,再放行端口,最后才 enable —— 避免自锁:
bash
sed -i 's/^DEFAULT_FORWARD_POLICY="DROP"/DEFAULT_FORWARD_POLICY="ACCEPT"/' /etc/default/ufw
ufw allow 22/tcp comment 'SSH'
ufw allow 80/tcp comment 'HTTP'
ufw allow 443/tcp comment 'HTTPS'
ufw --force enable- 备份:`/etc/default/ufw.bak.20261108-040*/
- 启用后立即用新连接验证 SSH 未被切断 ✅
2. 网络代理(04:08)
节点:订阅中 美国Z01 | IEPL | x1.5(Shadowsocks,aes-256-gcm)。
bash
apt-get install -y shadowsocks-libev privoxy
# 配置 /etc/shadowsocks-libev/us01.json (600)
# 自建 systemd 单元 ss-local-us01.service(root 运行)
systemctl enable --now ss-local-us01 privoxy踩坑:包自带的 shadowsocks-libev-local@.service 以非 root 运行, 读不到 600 权限配置文件,报 ERROR: Invalid config path. 并失败。 改用自建 unit 解决。
验证:出口 IP 39.107.41.224(北京)→ 142.249.36.77(美国洛杉矶); raw.githubusercontent.com 由 000 变 200。
3. 系统升级(04:10)
在独立 systemd 单元中后台执行,避免 SSH 超时中断:
bash
systemd-run --unit=tenodx-setup --collect --property=Type=oneshot \
--property=StandardOutput=append:/var/log/tenodx-setup.log \
/bin/bash /root/tenodx-setup.sh关键参数:DEBIAN_FRONTEND=noninteractive、NEEDRESTART_MODE=a(避免交互卡死)、 DPkg::Lock::Timeout=600、Dpkg::Options::=--force-confold(保留现有配置)。
结果:升级 18 个包;cloud-init、intel-microcode 因被 apt-mark hold 未升级(保留原状)。
4. Docker(04:11)
bash
apt-get install -y docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin
systemctl restart docker # ← 关键:应用新 daemon.json踩坑:apt install docker-ce 时包脚本会自动启动 daemon, 此时加载的是旧 daemon.json;systemctl enable --now 对已运行的 daemon 不生效。 必须 systemctl restart docker,否则镜像源配置不生效(docker info 仍显示旧列表)。
清理失效镜像源:原 9 个源中 6 个已死(mirror.baidubce.com、dockerproxy.com、 mirror.iscas.ac.cn、huecker.io、dockerhub.timeweb.cloud、noohub.ru), 保留实测可用的 6 个。
验证:docker pull hello-world ✅、docker pull alpine ✅、 容器内出网测试 ✅(证明 ufw FORWARD 放宽生效)、docker info 镜像源为新列表 ✅。
回滚:
bash
systemctl disable --now docker
apt-get purge -y docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin
rm -rf /var/lib/docker /var/lib/containerd2026-11-08 04:00 — 建立服务器信息档案
背景:将服务器现状固化为可维护的文档,便于后续部署与交接。
新增文件:
server-info/
├── README.md
├── inventory.yaml
├── 01-基础信息.md
├── 02-防火墙与端口.md
├── 03-软件清单.md
├── 04-服务与进程.md
├── 05-安全基线.md
├── 06-计划任务.md
├── 07-变更记录.md
├── scripts/collect.sh
├── scripts/refresh.ps1
└── raw/snapshot-20261108-0400.txt
raw/packages-20261108-0400.txt影响:无(只读采集,未改动服务器任何配置)。
2026-11-08 03:56~03:58 — SSH 免密登录 + 双向保活
背景:用户要求配置免密登录,并防止空闲超时断连。
1. 免密登录
| 项 | 内容 |
|---|---|
| 密钥类型 | ED25519 |
| 私钥 | C:\Users\40482\.ssh\id_ed25519 |
| 公钥 | C:\Users\40482\.ssh\id_ed25519.pub |
| 指纹 | SHA256:l433x+k/ErjgvBXvK+tp2Q5MrzgqgmSyyNwnaOuMT4w |
| 服务器位置 | /root/.ssh/authorized_keys(权限 600,.ssh 700) |
验证方式:BatchMode=yes + PasswordAuthentication=no + PreferredAuthentications=publickey 仍能登录 → 确认密钥认证生效。
2. 服务端保活
原值 clientaliveinterval 0(保活关闭),改为:
ini
# /etc/ssh/sshd_config.d/99-keepalive.conf
ClientAliveInterval 60
ClientAliveCountMax 10
TCPKeepAlive yessshd -t校验通过systemctl reload sshd- 生效确认:
clientaliveinterval 60/clientalivecountmax 10/tcpkeepalive yes - 备份:
/etc/ssh/sshd_config.bak.20261108-035642
3. 客户端保活
ssh-config
# C:\Users\40482\.ssh\config
Host *
ServerAliveInterval 30
ServerAliveCountMax 10
TCPKeepAlive yes
Host tenodx
HostName 39.107.41.224
User root
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
ServerAliveInterval 30
ServerAliveCountMax 10
TCPKeepAlive yes- 原配置(
ServerAliveInterval 60/CountMax 5)备份为config.bak.20261108-035615
效果:客户端每 30 秒、服务端每 60 秒发送保活探测,持续重置 NAT / 安全组空闲计时器; 任一方向网络中断约 10 分钟内判定死连接。
2026-10-30 — 人工运维(根据 /root/.bash_history 还原)
bash
grep -E "^(PasswordAuthentication|PubkeyAuthentication|PermitRootLogin)" /etc/ssh/sshd_config
sed -i 's/^PasswordAuthentication no/PasswordAuthentication yes/' /etc/ssh/sshd_config
systemctl restart sshd && echo "SSH password auth enabled"
sudo passwd root
reboot结论:此前人工开启了 SSH 密码登录并为 root 设置了密码。 这正是当前 PasswordAuthentication yes 的来源,也是系统能密码登录的原因。
⚠️ 该操作降低了安全性。既然密钥免密已配置好,建议改回
PasswordAuthentication no。
2026-09-16 14:22 — 系统初始化
- 使用阿里云官方镜像
ubuntu_22_04_x64_20G_alibase_20260916.vhd创建实例 wtmp begins Wed Sep 16 14:22:08 2026,为最早的系统记录
模板(新增变更时复制)
markdown
### YYYY-MM-DD HH:MM — 变更标题
**背景**:为什么要做这次变更
**操作**:
```bash
# 具体命令影响范围:影响了什么
验证:怎么确认成功
回滚:如何撤销
| 时间 | 操作人 | 变更内容 | 回滚方式 |
|---|---|---|---|
| | | | |