🎯 这篇文章解决什么问题
一台海外开发机同时放个人实验、客户项目、定时任务和生产服务时,最大的风险不是“目录看起来乱”,而是权限、密钥、日志和部署边界互相串线。一个项目的调试日志可能带出另一个项目的 Token;一个共享的 API Key 泄露后,也无法判断费用和影响来自哪里。
本文默认你已经会使用 .env 和 Git 忽略规则。基础做法请先看API Key 与 .env 安全管理。这里进一步解决长期运行、多项目和恢复演练问题。
适合
一台 VPS 上运行多个 Node.js、Python、Docker 或 AI API 项目。
目标
项目之间看不到彼此密钥,日志不泄露凭据,轮换可以回滚。
不包含
不讨论共享账号、批量注册、绕过平台限制或把个人 Key 分发给他人。
🧱 先确定四层隔离模型
不要把“不同目录”误当成完整隔离。至少要同时考虑下面四层:
- 身份层:每个长期运行项目使用独立 Linux 用户或容器服务账号,不用 root 常驻。
- 文件层:代码、数据、日志、运行时文件和密钥目录分开,权限只授予对应用户。
- 凭据层:开发、测试、生产使用不同的项目级 Key;不同应用也不要长期共用一把 Key。
- 运行层: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_KEY 与 OPENAI_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 交接:
- 在对应项目或环境创建新 Key,保持权限和费用边界最小化。
- 把新 Key 写入 Secret 存储,不通过聊天、工单或截图传递。
- 只更新一个 canary 实例或测试环境,执行最小请求、错误率和费用归属检查。
- 逐个更新生产实例,确认所有服务都不再使用旧 Key。
- 撤销旧 Key,再观察一段时间是否出现 401、失败重试或定时任务遗漏。
- 记录轮换日期、负责人、受影响服务和下一次轮换时间,不记录 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、备份、日志或错误配置读取;先撤销或轮换,再评估是否需要清理历史。
📚 官方资料来源
- OpenAI:保护账号并防止 API Key 泄露——用于核对环境变量、费用阈值、轮换和泄露处置口径。
- OpenAI:团队不共享个人 API Key——用于核对项目级 Key、环境隔离和用量归属。
- GitHub Push Protection——用于核对提交前阻止凭据进入仓库的能力。
- GitHub:从仓库移除敏感数据——用于核对先撤销凭据、再评估历史清理的顺序。
- GitHub Actions 安全使用参考——用于核对最小权限、日志遮盖和轮换要求。
- Docker Build Secrets 与 Docker Compose Secrets——用于核对构建期临时挂载和运行时按服务授权。