VPSKnow

AI 开发机多项目怎么隔离?日志脱敏、密钥轮换与恢复演练

中级
24分钟

🎯 这篇文章解决什么问题

一台海外开发机同时放个人实验、客户项目、定时任务和生产服务时,最大的风险不是“目录看起来乱”,而是权限、密钥、日志和部署边界互相串线。一个项目的调试日志可能带出另一个项目的 Token;一个共享的 API Key 泄露后,也无法判断费用和影响来自哪里。

本文默认你已经会使用 .env 和 Git 忽略规则。基础做法请先看API Key 与 .env 安全管理。这里进一步解决长期运行、多项目和恢复演练问题。

适合

一台 VPS 上运行多个 Node.js、Python、Docker 或 AI API 项目。

目标

项目之间看不到彼此密钥,日志不泄露凭据,轮换可以回滚。

不包含

不讨论共享账号、批量注册、绕过平台限制或把个人 Key 分发给他人。

🧱 先确定四层隔离模型

不要把“不同目录”误当成完整隔离。至少要同时考虑下面四层:

  1. 身份层:每个长期运行项目使用独立 Linux 用户或容器服务账号,不用 root 常驻。
  2. 文件层:代码、数据、日志、运行时文件和密钥目录分开,权限只授予对应用户。
  3. 凭据层:开发、测试、生产使用不同的项目级 Key;不同应用也不要长期共用一把 Key。
  4. 运行层:systemd、Docker Compose、CI/CD 各自只拿到该服务需要的 Secret,避免把整份宿主机环境透传进去。

隔离目标不是让运维变复杂,而是让一次泄露只影响一个项目、一个环境和一段时间。

👤 目录与 Linux 用户怎么拆

长期在线的项目建议使用固定服务用户。代码放在 /srv/<project>/app,持久数据、日志和运行时文件分开;密钥放在 /etc/<project>/,不要混入 Git 工作树。

  # 示例项目名统一使用 ai-demo,不包含真实客户或账号信息
sudo adduser --disabled-password --gecos "" ai-demo
sudo install -d -m 0750 -o ai-demo -g ai-demo /srv/ai-demo
sudo install -d -m 0750 -o ai-demo -g ai-demo /srv/ai-demo/{app,data,logs,run}

# 只有服务用户可读取密钥目录
sudo install -d -m 0700 -o ai-demo -g ai-demo /etc/ai-demo

如果只是短期个人实验,可以先用同一登录用户加独立目录和权限;一旦项目进入定时任务、systemd、多人维护或生产环境,就应升级为独立服务用户。

不要为了“方便”把项目目录和密钥目录设为 777。如果服务读不到文件,应检查所有者、用户组和具体读写目录,而不是取消权限边界。

🔐 密钥按运行层分别管理

systemd 服务

小型自托管项目可以用独立的 EnvironmentFile,文件权限设为 600,并让服务使用独立用户。支持 systemd credentials 的新系统可以进一步采用 LoadCredential=,避免把 Secret 作为普通环境变量长期暴露给整个进程环境。

  [Unit]
Description=AI demo service
After=network-online.target

[Service]
Type=simple
User=ai-demo
Group=ai-demo
WorkingDirectory=/srv/ai-demo/app
EnvironmentFile=/etc/ai-demo/runtime.env
ExecStart=/usr/bin/node server.js
Restart=on-failure
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/srv/ai-demo/data /srv/ai-demo/logs /srv/ai-demo/run

[Install]
WantedBy=multi-user.target

修改密钥文件后不要立刻删除旧 Key。先重启服务并完成一次最小 API 调用,再去提供商后台撤销旧 Key。

Docker Compose 运行时

Compose Secret 会按服务显式授权,并以文件形式挂载到 /run/secrets/。应用可以同时支持 OPENAI_API_KEYOPENAI_API_KEY_FILE,生产环境优先读文件。

  services:
  api:
    image: example/ai-api:latest
    secrets:
      - openai_api_key
    environment:
      OPENAI_API_KEY_FILE: /run/secrets/openai_api_key

secrets:
  openai_api_key:
    file: ./secrets/openai_api_key.txt

Docker 构建阶段

构建参数和普通环境变量可能进入镜像历史,不适合传递 npm Token、私有仓库凭据或 API Key。需要在构建时访问私有资源时,使用 BuildKit secret mount。

  # syntax=docker/dockerfile:1
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./

# Secret 只在这一条 RUN 中临时挂载,不写入镜像层
RUN --mount=type=secret,id=npm_token \
    NPM_TOKEN="$(cat /run/secrets/npm_token)" npm ci

COPY . .
CMD ["node", "server.js"]

# 构建命令
# docker build --secret id=npm_token,src=./secrets/npm_token.txt -t ai-demo .

GitHub Actions

CI 使用仓库、组织或 Environment Secrets,并把权限限制在需要的 job。不要在 workflow 中写真实值;调试时也不要直接输出整个环境。GitHub 能遮盖一部分已登记 Secret,但自定义日志、编码后的值和第三方工具输出仍可能泄露。

🧽 日志脱敏与排障边界

排障日志应该保留请求时间、模型或服务名、HTTP 状态码、耗时、重试次数和服务端 request ID;不应记录完整 Authorization Header、Cookie、请求正文中的隐私数据或未经编辑的环境变量。

  const SENSITIVE_KEYS = /authorization|api[-_]?key|token|cookie|password|secret/i;

export function redact(value, key = "") {
  if (SENSITIVE_KEYS.test(key)) return "[REDACTED]";
  if (Array.isArray(value)) return value.map(item => redact(item));
  if (value && typeof value === "object") {
    return Object.fromEntries(
      Object.entries(value).map(([childKey, childValue]) => [
        childKey,
        redact(childValue, childKey),
      ]),
    );
  }
  return value;
}

脱敏函数必须在日志序列化之前执行。仅在网页展示层打码是不够的,因为原始日志、APM、错误追踪和 CI Artifact 里仍可能保留真实值。

  • 开发日志默认只记录字段白名单,不先记录全部对象再做黑名单删除。
  • 截图前关闭终端历史、环境变量面板和请求 Header 展开区。
  • 临时提高日志级别要设置结束时间,排查完成后恢复,并检查已经上传的日志。
  • 如果 Secret 已进入公开日志,按泄露处理:先撤销或轮换,再删除日志和清理历史。

🔄 不中断服务的密钥轮换流程

轮换不是“生成新 Key 后马上删除旧 Key”。更稳妥的流程是短时间双 Key 交接:

  1. 在对应项目或环境创建新 Key,保持权限和费用边界最小化。
  2. 把新 Key 写入 Secret 存储,不通过聊天、工单或截图传递。
  3. 只更新一个 canary 实例或测试环境,执行最小请求、错误率和费用归属检查。
  4. 逐个更新生产实例,确认所有服务都不再使用旧 Key。
  5. 撤销旧 Key,再观察一段时间是否出现 401、失败重试或定时任务遗漏。
  6. 记录轮换日期、负责人、受影响服务和下一次轮换时间,不记录 Key 本身。

个人项目也建议把本地、远程开发机和生产服务拆成不同 Key。这样某台 VPS 丢失或某个仓库泄露时,不需要同时中断所有环境。

🚨 泄露恢复演练怎么做

至少每季度做一次不包含真实泄露的桌面演练:假设“某个项目 Key 出现在公开 Git 历史和 CI 日志”,然后验证谁能撤销、哪些服务依赖它、多久可以恢复。

0–15 分钟

确认 Secret 类型和暴露位置;撤销或轮换;暂停可能持续外泄的 workflow。

15–60 分钟

更新受影响服务;验证 API 调用、定时任务和告警;检查异常用量。

当天完成

删除公开日志和 Artifact;评估 Git 历史清理;通知协作者避免旧克隆重新污染。

事后改进

补 Secret 扫描、日志白名单、轮换清单和最小权限,记录恢复耗时。

GitHub 官方明确建议:如果泄露的是凭据,第一步是撤销或轮换。删除最新提交或改写历史不能让仍然有效的 Key 自动失效;历史改写还需要协调所有克隆和分支,避免再次污染。

上线前验证清单

  # 1. 确认密钥文件没有进入 Git
git status --short
git ls-files | grep -E '(^|/)(.env|secrets?)(.|/|$)' || true

# 2. 检查文件权限,只输出权限和文件名,不输出内容
stat -c '%a %U:%G %n' /etc/ai-demo/runtime.env

# 3. 确认服务以独立用户运行
systemctl show ai-demo --property=User,Group,EnvironmentFiles

# 4. 只验证变量是否存在,不回显真实值
sudo -u ai-demo test -r /etc/ai-demo/runtime.env && echo 'runtime env readable'
  • 每个长期服务都有明确的 Linux 用户、工作目录、可写目录和密钥来源。
  • 仓库只包含 .env.example,密钥、日志、备份包和调试截图没有被 Git 跟踪。
  • Docker 构建使用 secret mount,不使用 ARG 或普通 ENV 传递凭据。
  • 日志能定位错误码、耗时和 request ID,但不会输出 Header、Cookie、Token 和完整请求正文。
  • 轮换流程已在测试环境演练,旧 Key 撤销后能通过告警发现遗漏服务。
  • 负责人知道如何查看用量、撤销 Key、清理 CI 日志和联系平台支持。

常见问题

一个人维护的小项目也要创建多个 Linux 用户吗?

短期实验不一定需要。进入长期运行、自动部署、多项目共机或客户代码阶段后,独立服务用户的收益会明显增加。

环境变量是不是一定不安全?

环境变量比把 Key 写进代码更安全,但仍可能被调试输出、进程检查或错误报告带出。能使用平台 Secret、Compose Secret 或 systemd credential 时,可以进一步缩小暴露面。

Key 已经提交到私有仓库,需要轮换吗?

需要按已暴露处理。私有仓库仍可能被协作者、CI、备份、日志或错误配置读取;先撤销或轮换,再评估是否需要清理历史。

📚 官方资料来源