FluxIP/README.md

468 lines
23 KiB
Markdown
Raw Permalink 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.

# FluxIP
FluxIP 是一个使用 Python、FastAPI 和 SQLite 开发的 AWS Lightsail Static IP
轮换控制器。它可以管理多台 Lightsail 实例,把每台实例与一个 Cloudflare A 记录
组成一个托管目标再通过实例组统一设置轮换周期。A 记录默认使用 DNS only也可按
实例选择橙云模式。
轮换过程中不会停止或重启 Lightsail。FluxIP 会分配新的 Static IP解绑旧地址
把新地址附加到仍在运行的实例,验证 SOCKS5 或 HTTP 代理与 Cloudflare DNS等待释放宽限期
最后从 AWS 账号释放旧 Static IP。
配置、账号资料、任务状态和历史记录保存在 SQLite。可以分别维护多套 AWS 与
Cloudflare 账号每个托管实例独立选择要使用的两套账号。AWS 密钥与 Cloudflare
API Token 以及代理用户名、密码使用本机主密钥加密后存储API 和 WebUI 不会返回密钥明文。
## 重要边界
- 每台 Lightsail 实例只能附加一个 Static IP。地址切换不是双 IP 并行切流。
- 解绑旧地址和附加新地址之间会有短暂切换窗口。实例进程不会退出,但已有
SOCKS5 TCP 连接会断开,需要客户端重连。
- DNS only 模式下 Cloudflare 只负责 DNS 解析。DNS 缓存仍可能在 TTL 内返回旧地址,
FluxIP 不能提供零中断或连接迁移。
- 普通 Cloudflare 橙云不能转发 SOCKS5、通用 HTTP CONNECT 或 10808 等任意 TCP
端口。只有已经单独配置 Cloudflare Spectrum 等 TCP 产品时才应开启橙云FluxIP
只修改 DNS 记录的 `proxied` 状态,不会创建 Spectrum 应用。
- 橙云记录的 API 回读只能证明配置已经写入,不能证明所有 Cloudflare 边缘节点已经
切换源站;默认释放宽限期也不是 Spectrum 端到端可用性的保证。
- 旧 Static IP 一旦执行 `ReleaseStaticIp` 就不能找回。FluxIP 只会在新地址、DNS 和
可选代理认证与 CONNECT 检查通过后释放它。
- AWS 默认每个区域最多 5 个 Static IP配额可以申请提高。安全轮换采用
“先分配新地址、后释放旧地址”,因此同一区域需要至少一个空余配额。
- 调度器运行在 Web 进程内,只支持一个 Uvicorn worker 和一个应用副本。
- 生产环境建议把控制器部署到独立的长期在线主机。虽然 Static IP 切换不会关闭
目标实例,但同机部署会使 WebUI 管理连接也受地址切换影响。
## 资源模型
一个托管实例包含:
- 显示名称;
- AWS 账号、从内置中文名称与图标目录选择的 Lightsail 区域,以及 Lightsail 实例名称;
- Cloudflare 账号、Zone ID、唯一的完整 A 记录名和橙云开关;
- SOCKS5/HTTP 代理链接、端口、健康检查开关和超时;
- 旧地址释放宽限期;
- 是否允许参与手动或分组轮换。
一个实例组包含若干托管实例、启用状态和轮换间隔。到期时,组内已启用实例按照
成员顺序逐台轮换。串行执行只额外占用一个 Static IP 配额,也避免同时切断多台
代理。下次执行时间从整个批次结束后重新计算,两个批次之间至少间隔一个完整周期。
当前系统全局只允许一个轮换批次或 DNS 同步任务执行。
AWS 与 Cloudflare 账号分别建档,同一账号可以由多个实例复用,不同实例也可以选择
完全独立的账号组合。数据库会阻止以下冲突:
- 同一个 AWS 账号下的同一区域和 Lightsail 实例被重复管理;
- 同一个 Cloudflare 账号下的同一记录被多个托管实例使用;
- 同一个实例同时加入多个实例组。
账号的新增、修改和删除与实例配置共用轮换写锁。轮换批次或 DNS 同步任务执行期间
通常不能写入账号;任务进入待处理或待清理后,只允许替换当前运行项所绑定账号的
恢复密钥,不能改名、切换认证模式或操作其他账号。仍被活动实例引用的账号不能删除,
必须先把实例改绑到其他账号或删除相关实例。
## 轮换流程
一次实例轮换按以下顺序执行:
1. **预检**:确认 Lightsail 正在运行且有公网 IPv4读取当前附加的 Static IP
确认 Cloudflare A 记录的 IP、代理模式和 TTL 与实例配置一致。
2. **分配**:使用确定性任务资源名调用 `AllocateStaticIp`,轮询 AWS operation
再读取资源确认新 Static IP 已存在且属于正确区域。
3. **解绑**:如果实例原本已有 Static IP调用 `DetachStaticIp` 并回读确认旧地址
已解绑。实例原本使用动态 IP 时,这一步用于首次 Static IP 纳管,不存在旧静态
资源需要删除。
4. **附加**:调用 `AttachStaticIp` 把新地址附加到目标实例,并同时核对 Static IP
`attachedTo`、实例 `publicIpAddress``isStaticIp`
5. **健康检查**:直连新的 Static IP。SOCKS5 会完成方法协商、RFC 1929 用户密码认证
和外部 CONNECTHTTP 代理会发送带 Basic 认证的 CONNECT。粘贴链接中的域名不会
用于 DNS 切换前的源站检查,避免误测到旧 IP。
6. **更新 DNS**:把 Cloudflare A 记录切换到新地址;灰云写入 `proxied=false`、TTL 60
橙云写入 `proxied=true`、TTL Automatic然后回读验证。
7. **释放宽限**:至少等待 60 秒,默认 75 秒。旧地址此时已解绑,宽限期不会让旧
客户端继续访问,但可以避免旧地址在 DNS 缓存期立即被 AWS 分配给其他用户。
8. **删除旧地址**再次验证新地址仍附加、DNS 仍指向新地址,并按配置复查代理;
此时同时探测新 Static IP 和实际 A 记录域名,确保 DNS only 或 Spectrum 入口真正
可用后再调用 `ReleaseStaticIp`,以 `GetStaticIp` 返回不存在作为删除成功依据。
AWS 写操作是异步的。FluxIP 会保存任务阶段并轮询 `GetOperation`;进程意外退出后,
会根据 SQLite 状态和 AWS/Cloudflare 实际状态续跑,而不是仅根据上次 HTTP 响应猜测。
预检失败会直接结束任务;无法安全自动恢复的切换异常进入“待处理”;旧地址或回滚
新地址释放失败进入“待清理”。管理员应先核对云端状态,再更新凭据、继续或放弃任务。
如果旧 Static IP 仍存在、DNS 仍为旧地址且没有被外部修改FluxIP 会在新地址附加、
健康检查或 DNS 更新失败后自动执行持久化回滚:解绑新地址、重新附加旧地址、验证旧
代理与 DNS 路由,再删除本次分配的新 Static IP。回滚过程中每一步都会写入 SQLite
进程重启后可以幂等续跑。DNS 已经确认切到新地址时会继续向前验证,不会制造新旧 DNS
缓存分裂DNS 指向第三方地址、记录缺失或代理模式被外部改变时则停止自动操作。首次从动态 IP
纳管没有可重新附加的旧静态资源,因此不能自动回滚,只能保留现场并进入待处理。
## 运行要求
- Python 3.11 或更高版本;
- 可访问 AWS Lightsail API 和 Cloudflare API
- 目标 Lightsail 的 SOCKS5 或 HTTP 代理服务已经运行,端口和防火墙规则正确;
- Cloudflare 中使用独占的 A 记录;普通代理服务使用 DNS only橙云需要已配置 Spectrum
- AWS Static IP 配额具有轮换余量。
不允许使用多 worker
```text
--workers 1
```
不要运行多个 Compose 副本、多个 systemd 实例或多个指向同一数据库的控制器。
## 直接运行源码
### Windows PowerShell
```powershell
cd "D:\project\S5-IP自动轮换"
py -3.11 -m venv .venv
.\.venv\Scripts\python.exe -m pip install --upgrade pip setuptools
.\.venv\Scripts\python.exe -m pip install -r requirements.txt
if (-not (Test-Path .env)) { Copy-Item .env.example .env }
.\.venv\Scripts\python.exe -m app
```
打开 `http://127.0.0.1:8787`。关闭服务时在运行窗口按 `Ctrl+C`
### Linux 或 macOS
```bash
cd /path/to/fluxip
python3 -m venv .venv
.venv/bin/python -m pip install --upgrade pip setuptools
.venv/bin/python -m pip install -r requirements.txt
[ -f .env ] || cp .env.example .env
.venv/bin/python -m app
```
`python -m app` 会读取 `.env` 中的 `ROTATOR_HOST``ROTATOR_PORT` 和日志配置,并固定
使用一个 worker。需要临时覆盖时也可以继续使用显式的 Uvicorn 命令。
默认数据文件为:
```text
data/fluxip.db
data/master.key
```
首次启动会创建数据库和主密钥。数据库存在而主密钥丢失时,程序会拒绝启动,避免
用新密钥覆盖后无法解密原凭据。
从只支持一套全局凭据的旧版本升级时,已配置的 AWS 和 Cloudflare 凭据会在启动迁移
后分别导入为 legacy 账号,旧实例会自动绑定对应账号。导入过程使用现有主密钥解密并
重新加密,不会把密钥明文写入数据库或日志。
## 管理员账号
FluxIP **没有默认管理员账号或默认密码**
第一次打开 WebUI 时自行创建管理员,密码至少 10 位。未配置
`ROTATOR_BOOTSTRAP_TOKEN` 时,首次创建管理员只允许来自控制器回环地址;容器、远程
主机或公网反向代理部署应提前配置至少 32 字节高熵 Bootstrap Token。
生成示例:
```bash
python -c "import secrets; print(secrets.token_urlsafe(32))"
```
管理员创建完成后,可以从环境配置中删除 Bootstrap Token 并重启服务。不要把
Bootstrap Token、云端密钥或数据库主密钥提交到 Git。
## WebUI 首次配置
1. 创建管理员并登录。
2. 打开“账号”,新增 AWS 账号:选择默认凭据链,或填写专用 Access Key ID、Secret
Access Key临时凭据还需要 Session Token。
3. 继续新增 Cloudflare 账号并填写 API Token。可以为不同 AWS 或 Cloudflare 租户创建
多个独立账号资料。
4. 新建托管实例,选择已保存的 AWS 账号和 Cloudflare 账号;从带图标和中文名称的
区域目录选择 AWS Region再填写 Lightsail 实例名称、Zone ID 和完整 A 记录名。
代理测活链接可直接粘贴以下格式,协议、端口和认证会自动解析:
`socks5://用户:密码@域名:端口``http://用户:密码@域名:端口`
`socks5://域名:端口@用户:密码`。用户名或密码包含 `@``:` 时应使用 URL 编码。
链接里的域名必须与该实例的 A 记录完整域名一致。
5. 执行实例连接测试,确认所选 AWS 账号可以读取实例公网地址,所选 Cloudflare 账号
可以读取目标记录。
6. 如 DNS 尚未指向实例当前地址,先执行 DNS 同步。
7. 对单个实例执行一次手动轮换,核对新 Static IP、代理 CONNECT 和 DNS。
8. 创建实例组、添加成员、设置轮换间隔并启用计划。
配置更新使用版本号进行并发检查。如果页面数据已被另一会话修改,刷新后再提交。
账号、实例和实例组的删除是软删除,不会自动删除 Lightsail 实例或 Cloudflare Zone。
轮换批次或 DNS 同步执行中禁止新增、删除账号,也禁止修改实例或实例组。任务因凭据
失效进入待处理或待清理后,可以只替换当前运行项所绑定账号的恢复密钥;账号名称和
认证模式仍被锁定。此时也可以为当前实例重新粘贴相同域名、协议和端口的代理链接,
只替换加密的代理用户名与密码;其他实例配置仍被锁定。审计日志不记录密钥内容。
## AWS 最小权限
建议使用专用 IAM 身份或角色,不要使用 AWS 根账号密钥。FluxIP 当前需要以下
Lightsail API
- `GetInstance`
- `GetStaticIp`
- `GetStaticIps`
- `GetOperation`
- `AllocateStaticIp`
- `AttachStaticIp`
- `DetachStaticIp`
- `ReleaseStaticIp`
这些调用包含新资源分配、全区域静态地址查询和异步 operation 查询,不能全部稳定地
约束到一个预先存在的资源 ARN。下面策略使用精确 Action 列表和区域条件,避免授予
`lightsail:*`
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "FluxIPStaticIpRotation",
"Effect": "Allow",
"Action": [
"lightsail:GetInstance",
"lightsail:GetStaticIp",
"lightsail:GetStaticIps",
"lightsail:GetOperation",
"lightsail:AllocateStaticIp",
"lightsail:AttachStaticIp",
"lightsail:DetachStaticIp",
"lightsail:ReleaseStaticIp"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:RequestedRegion": [
"us-east-1",
"ap-southeast-1"
]
}
}
}
]
}
```
把区域列表改成实际使用的区域。单区域部署只保留一个值。组织若使用 SCP 或权限边界,
还需在相应层级允许这些 Action。权限不足时不要临时改成管理员权限应根据错误信息
补充缺失 Action 或区域。
### Static IP 配额
AWS 默认每个账号、每个 Lightsail 区域最多 5 个 Static IP配额可申请提高。假设同一
区域已有 `N` 台托管实例各占一个 Static IP安全轮换至少需要 `N + 1` 的配额。实例组
逐台执行,因此通常只需要一个临时余量。
如果 5 个地址已经全部占用FluxIP 不会先删除旧地址来腾配额。那样会失去恢复余地,
也可能重新获得相同地址。应先在 AWS 配额页面申请提高限制,或减少该区域的现用地址。
未附加的 Static IP 可能产生费用;待处理和待清理任务应尽快核对。
## Cloudflare 最小权限
创建专用 API Token不要使用 Global API Key。Token 只包含托管目标所在 Zone并授予
- `Zone / Zone / Read`
- `Zone / DNS / Edit`
多个实例跨多个 Zone 时,把这些特定 Zone 都加入同一 Token或改用覆盖所需 Zone 的
受控 Token。所有目标记录必须满足
- 类型为 A
- Proxy status 与实例里的橙云开关一致;
- 同名没有 CNAME、AAAA 或第二条 A 记录;
- 记录由 FluxIP 独占,不由其他自动化同时修改。
默认应使用 **DNS only灰云**。Cloudflare 普通橙云不代理任意 SOCKS5、通用
HTTP CONNECT 或 10808 等任意 TCP 端口。只有先在 Cloudflare 配置并开通 Spectrum
等对应 TCP 服务后才开启 FluxIP 的橙云开关;这个开关本身不会创建 Spectrum 应用。
FluxIP 写入前会核对当前记录,写入后再回读,但 Cloudflare 常规 DNS 接口不提供跨
AWS 与 DNS 的原子事务。需要人工接管时,先禁用相关实例组计划,并等待当前任务完成
或在任务详情明确处理异常现场。
## 环境变量
应用读取项目根目录 `.env`,变量统一使用 `ROTATOR_` 前缀。
| 变量 | 默认值 | 说明 |
| --- | --- | --- |
| `ROTATOR_ENVIRONMENT` | `development` | `production` 会关闭 API 文档 |
| `ROTATOR_HOST` | `127.0.0.1` | 服务监听地址 |
| `ROTATOR_PORT` | `8787` | 服务端口 |
| `ROTATOR_DATABASE_PATH` | `data/fluxip.db` | SQLite 文件 |
| `ROTATOR_MASTER_KEY_FILE` | `data/master.key` | 凭据加密主密钥文件 |
| `ROTATOR_MASTER_KEY` | 空 | 外部注入的 URL-safe Base64 主密钥,优先于文件 |
| `ROTATOR_BOOTSTRAP_TOKEN` | 空 | 远程首次初始化令牌 |
| `ROTATOR_COOKIE_SECURE` | `false` | HTTPS 生产环境应设为 `true` |
| `ROTATOR_SESSION_DAYS` | `7` | 管理员会话有效期 |
| `ROTATOR_ALLOWED_ORIGINS` | 本机 8787 | 允许的完整 Origin逗号分隔 |
| `ROTATOR_TRUSTED_HOSTS` | `localhost,127.0.0.1` | 接受的 Host逗号分隔 |
| `ROTATOR_TRUSTED_PROXIES` | `127.0.0.1,::1` | 可提交转发头的代理 IP/CIDR |
| `ROTATOR_SCHEDULER_ENABLED` | `true` | 是否执行实例组到期扫描 |
| `ROTATOR_SCHEDULER_TICK_SECONDS` | `10` | 扫描间隔2 到 60 秒 |
| `ROTATOR_LOG_LEVEL` | `INFO` | 日志级别 |
| `ROTATOR_EXTERNAL_TIMEOUT_SECONDS` | `20` | 外部请求基础超时5 到 60 秒 |
生产示例见 `deploy/fluxip.env.example`。不要把生产密钥直接写进仓库内的示例文件。
## Docker Compose
Docker Compose 默认把 WebUI 发布到 `127.0.0.1:8787`,并使用项目目录下三个独立的
宿主机目录保存数据库、主密钥和备份:
```text
data/fluxip-data/
data/fluxip-key/
data/fluxip-backups/
```
部署需要 Docker Compose v2使用 `docker compose` 命令)。启动时,一次性的
`fluxip-init` 服务会拒绝挂载目录中的符号链接或特殊文件,把目录权限收紧并交给
UID/GID `10001` 后退出;长期运行的 `fluxip` 服务仍是非 root 用户。已有数据库、
主密钥和备份不会被初始化服务删除或改写内容。
```bash
cp deploy/fluxip.env.example .env
# 编辑 .env至少设置高熵 ROTATOR_BOOTSTRAP_TOKEN
docker compose up -d --build
docker compose ps -a
curl http://127.0.0.1:8787/readyz
```
Windows PowerShell
```powershell
Copy-Item deploy\fluxip.env.example .env
docker compose up -d --build
Invoke-RestMethod http://127.0.0.1:8787/readyz
```
常用命令:
```bash
docker compose logs -f --tail=200 fluxip
docker compose restart fluxip
docker compose down
```
`docker compose down` 不删除上述宿主机目录。不要手工删除 `data/fluxip-key/master.key`
数据库存在但主密钥丢失时,已保存的云端凭据将无法解密。
## Linux systemd 与 HTTPS
示例假定代码位于 `/opt/fluxip`
```bash
sudo useradd --system --home /var/lib/fluxip --shell /usr/sbin/nologin fluxip
sudo install -d -o root -g root -m 0755 /opt/fluxip /etc/fluxip
sudo cp -a app migrations deploy requirements.txt pyproject.toml README.md /opt/fluxip/
sudo python3 -m venv /opt/fluxip/.venv
sudo /opt/fluxip/.venv/bin/python -m pip install -r /opt/fluxip/requirements.txt
sudo install -o root -g root -m 0600 deploy/fluxip.env.example /etc/fluxip/fluxip.env
sudo editor /etc/fluxip/fluxip.env
sudo install -o root -g root -m 0644 deploy/fluxip.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now fluxip.service
curl http://127.0.0.1:8787/readyz
```
查看日志:
```bash
sudo journalctl -u fluxip.service -n 200 --no-pager
```
`deploy/nginx-fluxip.conf.example` 是本机 Nginx HTTPS 反向代理示例。启用后设置:
```dotenv
ROTATOR_COOKIE_SECURE=true
ROTATOR_ALLOWED_ORIGINS=https://controller.example.com
ROTATOR_TRUSTED_HOSTS=controller.example.com,127.0.0.1
ROTATOR_TRUSTED_PROXIES=127.0.0.1,::1
```
若代理不在本机,只填写代理的精确 IP/CIDR并同步限制 Uvicorn
`--forwarded-allow-ips` 和防火墙。不要信任 `0.0.0.0/0``::/0` 的转发头。
## 备份与恢复
数据库和主密钥必须成对恢复,但不应默认放进同一个未加密归档:
- 数据库包含管理员、实例、实例组、任务历史和加密凭据;
- `master.key` 可以解密云端凭据,必须单独保存在密码库或加密介质;
- 丢失主密钥无法恢复凭据,泄露主密钥会使数据库密文失去保护;
- 不要提交数据库、主密钥、`.env` 或备份包。
在线备份只打包 SQLite
```bash
python3 deploy/backup.py \
--database /var/lib/fluxip/fluxip.db \
--output-dir /var/backups/fluxip \
--keep-last 30
```
Docker
```bash
docker compose exec -T fluxip python /app/deploy/backup.py \
--database /app/data/fluxip.db \
--output-dir /app/backups \
--keep-last 30
docker compose cp fluxip:/app/backups/. ./fluxip-backups
```
systemd 定时备份示例为 `deploy/fluxip-backup.service`
`deploy/fluxip-backup.timer`。备份后应验证同名 `.sha256`,并定期在隔离环境实际恢复。
只有目标目录本身已经加密时,才使用 `--allow-plaintext-key-archive` 创建包含主密钥的
完整归档。恢复时先停止 FluxIP同时恢复匹配的数据库和主密钥再启动并验证
`/readyz`、管理员登录、账号资料状态、实例组计划和云端连接。
## 健康检查与排错
- `GET /healthz`:进程存活状态,返回 `status` 和应用版本;不探测 SQLite 或云端账号。
- `GET /readyz`:检查 SQLite 和已启用调度器的运行状态;就绪时返回 HTTP 200数据库
异常或已启用调度器意外退出时返回 HTTP 503 和 `degraded`。响应中的 `scheduler`
会显示开关、运行状态、最近扫描/派发/异常时间;该接口不会调用 AWS 或 Cloudflare。
- **到期后没有自动轮换**:先检查 `/readyz``scheduler.enabled``running`。系统环境
变量优先于 `.env`;请删除意外继承的 `ROTATOR_SCHEDULER_ENABLED=false`,或明确设为
`true` 后重启。已逾期的实例组会在调度器启动后的首轮扫描立即执行。
- **没有默认管理员**:首次打开页面自行创建;远程初始化需 Bootstrap Token。
- **账号连接测试失败**:确认实例选中了正确类型的 AWS 与 Cloudflare 账号,账号密钥
完整,并且 Region、Zone ID 和记录名属于对应账号。
- **AWS AccessDenied**:核对所选 AWS 账号的上述 8 个 Lightsail Action 和目标区域,
不要直接授予管理员。
- **Static IP 配额不足**:提高该区域配额;不要先释放正在使用的旧地址。
- **找不到附加的旧地址**确认实例、Static IP 和区域一致,避免同时在控制台人工修改。
- **代理检查失败**:确认服务监听公网接口、端口开放、链接协议与服务一致,并重新粘贴
正确的用户名、密码。探测会实际建立到 `1.1.1.1:443` 的 CONNECT。
- **DNS 冲突**:删除同名 CNAME、AAAA 或多余 A 记录,并让橙云状态与实例配置一致。
- **任务待处理**:先核对新旧 Static IP 的真实附加状态和 DNS再更新恢复凭据或继续。
- **任务待清理**:新地址通常已经生效,但旧地址尚未成功释放,应尽快处理以避免费用。
- **客户端短暂掉线**Static IP 替换会终止原 TCP 连接,这是当前架构的预期边界。
- **主密钥缺失**:恢复与数据库匹配的主密钥,不要删除数据库让程序生成新密钥。
- **Docker 启动提示 `/app/secrets/master.key` 无权限**:拉取最新代码后执行
`docker compose down`,再执行 `docker compose up -d --build --force-recreate`,确认
`docker compose ps -a``fluxip-init``Exited (0)`。这两个命令不会删除持久化
目录。旧版本可先停止服务,再将 `data/fluxip-data``data/fluxip-key`
`data/fluxip-backups` 的所有者递归改为 `10001:10001`;不要删除或单独替换已有主密钥。
## 安全清单
- Uvicorn 始终使用单 worker、单副本。
- WebUI 只开放给受信网络,公网入口使用 HTTPS。
- AWS 使用专用最小权限身份Cloudflare Token 只授权必要 Zone。
- 每个 Cloudflare A 记录由一个托管实例独占;普通 SOCKS5/HTTP CONNECT 使用 DNS only。
- 代理服务启用身份认证并限制入站来源。
- 为 Static IP 配额预留至少一个安全轮换余量。
- 定期检查待处理、待清理任务和未附加 Static IP 费用。
- SQLite 与主密钥成对备份,备份异机存放并加密。
- 不在日志、截图、工单、Git 或最终交付文档中发送真实密钥。