Skip to content

07 — 变更记录 ​

按时间倒序记录所有对服务器的变更。每次操作后请追加一行。


变更一览 ​

时间 (CST)操作人变更内容回滚方式
2026-11-08 09:35DSHTenoDX 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:35DSH实机验证:exe 在 COM25 正确识别 H503 控制器(USB 序列 3514343932345103,Magic 能力 0x01 0x02)—
2026-11-08 05:40DSH部署 VitePress 文档站 tenodx-docs(构建进镜像 + 容器内 Caddy 托管,23 篇 md);挂到 Caddy docs.localhost;纳入监控(第 12 项)cd /opt/stacks/docs && docker compose down;恢复 Caddyfile.bak.*
2026-11-08 05:33DSH新增 docs-site/(VitePress 配置 + prepare.ps1 内容装配 + Dockerfile/compose)删除该目录
2026-11-08 05:32DSH新增 tools/TenoDXHub/(H503 本地 Hub:控制器信息 + 固件拉取刷入)删除该目录
2026-11-08 05:30DSH固件资源服务器挂到 Caddy(fw.localhost)并纳入监控(第 11 项);tunnel.ps1 新增 fw 服务恢复 Caddyfile.bak.*
2026-11-08 05:28DSH部署固件资源服务器 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:25DSHCaddy 纳入监控(第 10 项);tunnel.ps1 新增 web 服务并修复逗号参数解析—
2026-11-08 05:22DSH修复 Caddy basic auth 恒 401:bcrypt 哈希被 compose $ 插值破坏,改为内联进 Caddyfile恢复 compose.yaml 环境变量写法(会复现故障)
2026-11-08 05:18DSH部署 Caddy v2.11.9 作为 loopback Web 入口:Host 路由 + basic auth + 落地页 + /healthzcd /opt/stacks/caddy && docker compose down -v
2026-11-08 05:13DSH新增 backup-stacks.sh + cron(每日 03:35 备份栈配置 / kuma.db / NapCat 配置)rm /etc/cron.d/tenodx-stack-backup
2026-11-08 05:11DSHUptime Kuma 9 项监控全部就位(8 启用 + 1 待 QQ 登录),含宿主 cron 推送磁盘/代理见 11-应用服务.md
2026-11-08 05:04DSH配置 NapCat OneBot11 HTTP(3000)/WS(3002),发布端口(仅 loopback)改回 compose 端口映射
2026-11-08 05:02DSH部署 NapCat 容器(mlikiowa/napcat-docker,privileged,2.16 GB 镜像)cd /opt/stacks/napcat && docker compose down -v
2026-11-08 05:00DSH部署 Uptime Kuma 2.5.9(SQLite,跳过 2.x 向导)cd /opt/stacks/uptime-kuma && docker compose down -v
2026-11-08 04:33DSH新增 scripts/tunnel.ps1(本机 SSH 隧道助手,绕过安全组访问 loopback 服务)删除该文件
2026-11-08 04:32DSHPostgreSQL 备份恢复演练通过:1000 行 → 备份 → 删表 → 恢复到临时库 → 1000 行完整还原;docker 守护进程重启后容器自动恢复无变更(演练后已清理临时库与测试表)
2026-11-08 04:30DSH外部端口探测(73 端口):确认安全组仅放行 22/tcp + ICMP,80/443 被阻断,Web 服务当前无法对外无变更(只读探测)
2026-11-08 04:22DSH部署 PostgreSQL 18 容器(tenodx-postgres),数据卷 tenodx-pgdata,仅绑 127.0.0.1:5432;建库 tenodx/用户 tenodxcd /opt/stacks/postgres && docker compose down -v
2026-11-08 04:22DSH新增 /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:22DSHeffective_cache_size 调为 768MB(默认按更大内存估算,对本机偏高)ALTER SYSTEM RESET effective_cache_size;
2026-11-08 04:21DSH拉取 postgres:18-alpine 镜像docker rmi postgres:18-alpine
2026-11-08 04:20DSH立规:新增 10-部署规范.md —— 软件一律 Docker 运行(除非必裸装)删除该文档
2026-11-08 04:17DSH安装 fail2ban 0.11.2(sshd jail,白名单运维 IP);添加 2 GB swapfile 并写入 fstab;vm.swappiness 0→10apt purge fail2ban;swapoff /swapfile && rm /swapfile + 删 fstab 行
2026-11-08 04:16DSH关闭 SSH 密码登录 PasswordAuthentication no + X11Forwarding no,已 reload 并验证密钥登录正常恢复 /root/sshd-99-keepalive.backup-20261108-041557.conf 后 systemctl reload sshd
2026-11-08 04:16DSH新增 09-决策记录.md,记录全部自主决策与理由—
2026-11-08 04:12DSH更新档案:新增 08-网络代理.md,刷新 02/03/05 与 inventory.yaml—
2026-11-08 04:12DSH安装 /usr/local/bin/px 代理使用助手删除该文件
2026-11-08 04:11DSH安装 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:10DSH系统升级:apt update/upgrade/dist-upgrade/autoremove,升级 18 个包(cloud-init、intel-microcode 因被 hold 未升级)快照回滚
2026-11-08 04:08DSH安装 shadowsocks-libev + privoxy,自建 ss-local-us01.service(SOCKS5 127.0.0.1:1080);privoxy 转 HTTP 代理 127.0.0.1:8118systemctl disable --now ss-local-us01 privoxy
2026-11-08 04:05DSH启用 ufw 防火墙:放行 22/80/443;DEFAULT_FORWARD_POLICY DROP → ACCEPT(供 Docker)ufw disable;恢复 /etc/default/ufw.bak.*
2026-11-08 04:00DSH建立服务器信息档案 server-info/删除目录
2026-11-08 03:58DSH本机 ~/.ssh/config 写入保活配置 + tenodx 别名恢复 config.bak.20261108-035615
2026-11-08 03:57DSH本机 known_hosts 写入服务器 3 个主机公钥删除对应行
2026-11-08 03:56DSH服务端新建 /etc/ssh/sshd_config.d/99-keepalive.conf(ClientAliveInterval 60 / CountMax 10),sshd -t 校验后 reload删除该文件后 systemctl reload sshd
2026-11-08 03:56DSH生成 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/firmwareX-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-registry

2026-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 有凭据 /dashboard200,<title>Uptime Kuma
napcat.localhost 有凭据 /webui/200,<title>NapCat WebUI
/healthz200 ok
/favicon.ico(穿透静态资源)200

5. 回滚 ​

bash
cd /opt/stacks/caddy && docker compose down -v
rm -rf /opt/stacks/caddy

2026-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 次:

  1. 2.x 首次启动停在 Waiting for user action...(数据库选择向导,无法无头完成) → 用 UPTIME_KUMA_DB_TYPE=sqlite 跳过。
  2. 设了环境变量后仍报 no such table: setting → 残留的 0 字节 kuma.db 被误判为"已有库",迁移没执行。删掉空文件后正常建表。
  3. 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 个,都会让监控静默失效) ​

#坑现象正解
1TCP 类型名是 port 不是 tcpUnknown Monitor Type,看起来像服务挂了数据库 type='port'
2容器内 127.0.0.1 是自己ECONNREFUSED 127.0.0.1:5432用容器名 tenodx-postgres:5432
3docker-proxy 先接受 TCP 握手对发布端口用 port 类型永远显示 UP(假阳性)改用 http 类型
4push 的 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:2

2026-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=1row-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-alpine

2026-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 -Tpasswordauthentication 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    = normal
  • banaction = 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 sshd

2026-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/containerd

2026-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 yes
  • sshd -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
# 具体命令

影响范围:影响了什么

验证:怎么确认成功

回滚:如何撤销


| 时间 | 操作人 | 变更内容 | 回滚方式 |
|---|---|---|---|
| | | | |

TenoDX · 内部文档