一天时间,把乱七八糟的订阅和服务器收拾干净
精简 AI 订阅、迁移到 1Password、域名统一到 Cloudflare、加固 VPS,再让云端 AI Agent 安全地接管服务器。
我手上的东西越攒越多:好几个 AI 订阅、一台 VPS、两个域名,还有学校里一堆要维护的机器。东西一多就容易忘:哪个订阅什么时候扣钱、密码存在哪、服务器上跑着什么,全靠脑子记。
今天花了一天,把这些从头整理了一遍。这篇记录一下最后的方案。
整理思路:分三层
动手之前先想清楚要管什么,我把它分成三层:
- 资产清单:我到底有什么?每样东西花多少钱、什么时候续费、拿来干嘛
- 凭据管理:密码、SSH key、API token 放在哪
- 访问与运维:人和 AI Agent 怎么安全地操作服务器
原则只有一条:秘密只放在密码管理器里,配置文件里只写”指向秘密的指针”,清单里只写”秘密在哪”。
第一层:资产清单和订阅精简
我建了一个私有 Git 仓库 infra,作为所有资产的唯一信息源:
infra/
├── inventory.yaml # 订阅、服务器、域名:费用、续费日、用途
├── servers/hk.md # 服务器档案:系统、用户、SSH 配置、变更记录
├── domains/README.md # 域名:注册商、DNS、到期日
├── cloudflare.md # Zone、Tunnel、Access 应用、Token
├── agents.md # 各个 AI 工具的分工和权限边界
├── TODO.md
└── scripts/ # 配置脚本(不含任何密钥)
inventory.yaml 里每一项大概长这样:
- name: Claude Pro
cost: 20
cycle: monthly
status: active
role: 本地 agent(代码、运维、文档)
credentials: 1Password → Personal → Claude
列出来之后才发现,好几个 AI 订阅的功能是重叠的。最后定下的分工是:
| 工具 | 角色 | 费用 |
|---|---|---|
| Claude | 本地 agent:写代码、运维、整理文档 | $20/月 |
| Memoh | 云 agent:管理服务器和 Cloudflare | $5/月 |
| Meta Muse | 生活助手 | 免费 |
其他订阅到期不再续费,每月固定支出从大约 28。
另外给 AI 工具划了边界:生活助手不接触任何服务器信息;云 agent 只拿单独的、可以随时撤销的凭据;学校内网完全手动管理,不接入任何云 agent。
第二层:迁移到 1Password
之前密码都存在 Google 密码管理器里。迁移过程很简单:从 Google 导出 CSV,在 1Password 网页版里导入,然后立刻删掉那个 CSV,因为里面全是明文。
然后按用途建了三个保管库(Vault):
- Personal:日常网站账号
- Infra:服务器 SSH key、root 密码、Cloudflare token
- School:学校的东西,和个人的完全隔离
SSH Agent:私钥不落盘
1Password 最让我惊喜的是 SSH Agent。SSH 私钥直接在 1Password 里生成和保存,硬盘上没有私钥文件。每次 ssh 时 1Password 弹出 Touch ID,按一下就完成签名。
# 在 1Password 里生成 key,只把公钥导出到本地
op item create --category=ssh --title="hk" --vault=Infra --ssh-generate-key=ed25519
op read "op://Infra/hk/public key" > ~/.ssh/hk.pub
默认情况下 Agent 只提供 Personal 里的 key,需要在 ~/.config/1Password/ssh/agent.toml 里加上其他保管库:
[[ssh-keys]]
vault = "Personal"
[[ssh-keys]]
vault = "Infra"
~/.ssh/config 里有个小技巧:IdentityFile 指向的是公钥。私钥不在硬盘上,写公钥是为了告诉 Agent “只用这一把”,否则 Agent 会把所有 key 挨个试一遍。
Host hk
HostName <服务器 IP>
User deploy
IdentityAgent ~/.1password/agent.sock
IdentityFile ~/.ssh/hk.pub
IdentitiesOnly yes
Secret Reference:配置里只写引用
1Password CLI 支持 op://保管库/条目/字段 这种引用写法。脚本和配置文件里只写引用,运行时再读取真实值:
op read "op://Infra/hk deploy/password"
这样脚本就能放心提交到 Git,里面没有任何秘密。后面所有的配置脚本都是这么写的。
第三层:域名统一到 Cloudflare
两个域名注册在阿里云,DNS 全部改成 Cloudflare 的 nameserver。以后的小站打算放到 Cloudflare Pages / Workers 上,统一在一个后台管理。
服务器加固
新买的 VPS 是 Debian 13,做了这几件事:
1. 只允许 key 登录,禁用 root
建一个普通用户 deploy 负责日常运维,加入 sudo 组。加固配置没有直接改 /etc/ssh/sshd_config,而是单独放在 sshd_config.d/ 下:
# /etc/ssh/sshd_config.d/00-hardening.conf
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
sshd 对同一个配置项取第一次出现的值,而 sshd_config.d/*.conf 在主配置文件的开头就被 Include 了,00- 开头的文件又最先加载,所以它会优先生效。自己的修改集中在一个文件里,一目了然,也不怕主配置被系统或商家的工具覆盖。
2. sudo 需要密码,但不用一直输
deploy 的 sudo 密码由 1Password 随机生成。这样就算 SSH key 被盗用,也拿不到 root 权限。为了不那么烦,延长了免密时间:
# /etc/sudoers.d/10-deploy-timeout
Defaults:deploy timestamp_timeout=60
需要时 Cmd+Shift+Space 唤出 1Password 搜索框,复制密码,和用 Spotlight 启动应用的手感一样。
让云端 AI Agent 安全地接管服务器
这是今天最有意思的部分。Memoh 是一个云托管的 agent,本质上是一台完整的 Linux 虚拟机。问题是:它没有固定的出口 IP,没法用 IP 白名单来限制它。
我对比了 Tailscale 和 Cloudflare Tunnel,最后选了后者:
- 我的 Mac 不需要常驻任何进程,
cloudflared只在 SSH 时临时启动 - 可以对 Agent 和我自己分别设置访问策略,单独撤销
- 和域名、Pages 都在 Cloudflare 一个后台
整体结构是这样的:
Memoh 虚拟机 ──Service Token──→ Cloudflare Access ──→ Tunnel ──→ VPS(memoh 用户)
我的 Mac ──邮箱登录────────→ Cloudflare Access ──→ Tunnel ──→ VPS(deploy 用户)
服务器端:Tunnel
服务器上运行 cloudflared,主动连接 Cloudflare,把 ssh.example.com 转发到本机的 localhost:22。Tunnel 的 token 放在只有 root 能读的文件里,不出现在命令行和 systemd 配置中:
# /etc/systemd/system/cloudflared.service
[Service]
EnvironmentFile=/etc/cloudflared/token.env
ExecStart=/usr/local/bin/cloudflared --no-autoupdate tunnel run
Restart=on-failure
访问控制:Cloudflare Access
给 ssh.example.com 建一个 Access 应用,配两条策略:
- Allow:我自己的邮箱,用邮件验证码登录
- Service Auth:Memoh 专用的 Service Token
Agent 端:两道门
Memoh 要登录服务器,需要同时通过两道门:
- Cloudflare Access:出示 Service Token
- sshd:出示它自己的 SSH key
SSH key 是在 Memoh 虚拟机里生成的,私钥从来没有离开过那台机器,我只拿到了公钥。服务器上给它建了一个独立的 memoh 用户,不给 sudo,公钥前面加上 restrict:
restrict,pty ssh-ed25519 AAAA... memoh@hk
restrict 会禁用端口转发、Agent 转发、X11 转发,这把 key 只能开一个普通终端执行命令,没法把服务器当成跳板。
Memoh 那边的 ~/.ssh/config:
Host hk
HostName ssh.example.com
User memoh
IdentityFile ~/.ssh/hk_memoh
StrictHostKeyChecking yes
ProxyCommand sh -c '. ~/.config/cf-access/hk.env && exec cloudflared access ssh --hostname %h --service-token-id "$CF_ACCESS_CLIENT_ID" --service-token-secret "$CF_ACCESS_CLIENT_SECRET"'
服务器的主机公钥也提前写进了 Memoh 的 known_hosts,第一次连接就能验证对方的身份。
从服务器日志能看到,Memoh 的连接来源是 ::1,也就是确实经过 Tunnel 进来的,没有走公网:
Accepted publickey for memoh from ::1 port 40948 ssh2: ED25519 SHA256:...
想收回权限时,在 Cloudflare 后台删掉 Service Token,或者删掉服务器上那一行公钥,任意一个都行,几秒钟的事,不影响其他任何东西。以后它需要重启某个服务了,再通过 sudoers 白名单只开放具体的几条命令。
总结
一天下来,最大的收获不是某个工具,而是几条原则:
- 一份清单:所有东西记在一个地方,人和 agent 都读它
- 秘密集中:只在 1Password 里,其他地方只写引用
- 最小权限:每个使用方一把独立的凭据,只给需要的权限
- 随时可撤销:出了问题能在几秒钟内收回,而不影响其他东西
接下来还想做的:等 Tunnel 稳定用一段时间,把公网的 22 端口彻底关掉;给 VPS 配上快照和备份;把博客迁到 Cloudflare Pages 上。