服务器/仓库明文密钥 3 步自查:找出来、查 Git 历史、先换再删

作者: OpsMate 团队
更新于
命令请在你有权限的服务器和仓库上执行,路径与服务名按环境替换。

简短回答:先在服务器上只列出位置和配置项名字(不打印值):.env 和配置文件、Shell 历史、docker inspect 里的环境变量、compose 文件、crontab 和 systemd 服务。再用 gitleaks 或 trufflehog 扫一遍 Git 仓库和全部历史。查到的密钥先换新的、再删旧的;已经泄露过的密钥,删提交、改写历史都不能让它失效,只有更换才行。

数据库密码写在 .env 里,部署 token 留在 Shell 历史里,云账号 AccessKey 曾经提交进仓库又被删掉:这些都是小团队服务器上常见的明文密钥。本文给出一个 3 步自查:找出来、查清 Git 历史、按正确顺序处置,最后一节讲把日志和终端输出发给任何 AI 工具前怎么脱敏。

使用前先看这几条:

1. 服务器上:明文密钥藏在哪

【只读】 .env 文件和配置项名字:

sudo find /root /home /srv /opt /var/www /etc -xdev \( -name '.env' -o -name '.env.*' -o -name '*.env' \) -type f -not -path '*/node_modules/*' 2>/dev/null
sudo grep -oE '^(export +)?[A-Za-z_][A-Za-z0-9_]*=' /path/to/app/.env

【只读】 配置文件里写死的密码和 token(只列文件名):

sudo rg -l -i --hidden --no-ignore -g '!node_modules' -g '!*.log' '(password|passwd|secret|token|api[_-]?key|access[_-]?key)\s*[:=]' /etc /srv /opt /var/www /home /root 2>/dev/null
sudo grep -rIlEi --exclude-dir=node_modules '(password|passwd|secret|token|api[_-]?key|access[_-]?key)[[:space:]]*[:=]' /etc /srv /opt /var/www /home /root 2>/dev/null

两条做同一件事,有 rg(ripgrep)用第一条,更快;没有就用第二条。-l 只列出命中的文件名,不打印内容。命中很多是正常的,比如配置模板和注释;重点看你自己应用的目录。想看某个文件命中了哪些配置项名字,而不看值:

sudo grep -noiE '[A-Za-z0-9_]*(password|passwd|secret|token|api_?key)[A-Za-z0-9_]*' /path/to/app/config.yml

【只读】 私钥和各类工具的凭证文件:

sudo grep -rlE -- '-----BEGIN ([A-Z]+ )?PRIVATE KEY-----' /root /home /srv /opt /var/www /etc 2>/dev/null
sudo ls -la /root/.git-credentials /root/.netrc /root/.aws/credentials /root/.docker/config.json /home/*/.git-credentials /home/*/.netrc /home/*/.aws/credentials /home/*/.docker/config.json 2>/dev/null

【只读】 Shell 历史里敲过的密码(只看文件名和行号):

sudo grep -nEi '(password|passwd|secret|token|api[_-]?key)=|mysql .*-p[^[:space:]]|curl .*(-u |authorization:)|export [A-Z_]*(KEY|TOKEN|SECRET|PASSWORD)' /root/.bash_history /home/*/.bash_history /root/.zsh_history /home/*/.zsh_history 2>/dev/null | cut -d: -f1,2

cut -d: -f1,2 只保留“文件名:行号”,不显示命令本身。常见来源:mysql -pXXX、curl -u user:XXX、export API_TOKEN=XXX。

【只读】 Docker 容器的环境变量(只看名字):

for c in $(docker ps -aq); do
  echo "== $(docker inspect --format '{{.Name}}' "$c")"
  docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' "$c" | cut -d= -f1 | grep -Ei 'pass|secret|token|key|dsn|url'
done

传给容器的环境变量,任何能执行 docker inspect 的人都能看到明文值,能用 docker 命令基本等于有 root 权限。同理,构建镜像时用 ENV 或 ARG 写进去的密钥,会留在镜像层里,docker history --no-trunc 能看到,这条的输出不要外传。

【只读】 compose 文件里直接写的值:

sudo find /root /home /srv /opt -xdev \( -name 'docker-compose*.yml' -o -name 'docker-compose*.yaml' -o -name 'compose.yml' -o -name 'compose.yaml' \) -not -path '*/node_modules/*' 2>/dev/null
sudo grep -Ei '(password|secret|token|api_?key)[A-Za-z0-9_]*[[:space:]]*[:=]' /path/to/app/docker-compose.yml | grep -vc '\${'

第二条数一数“带密码类名字、又没有用 ${变量} 引用”的行数。结果是 0 说明值都从 .env 或别处引用;大于 0 说明有值直接写在 compose 文件里,而 compose 文件往往会进 Git 仓库。

【只读】 crontab 和 systemd 服务(只看文件名和行号):

sudo grep -nEi '(password|passwd|secret|token|api_?key|authorization)' /etc/crontab /etc/cron.d/* /var/spool/cron/crontabs/* /var/spool/cron/* 2>/dev/null | cut -d: -f1,2
sudo grep -rnEi '^(Environment=.*(pass|secret|token|key)|EnvironmentFile=)' /etc/systemd/system /lib/systemd/system 2>/dev/null | cut -d: -f1,2

【只读】 谁能读这些文件:

sudo find /root /home /srv /opt /var/www -xdev \( -name '.env' -o -name '.env.*' -o -name '*.pem' -o -name '*.key' \) -type f \( -perm -o=r -o -perm -g=r \) -exec ls -l {} + 2>/dev/null

列出同组或所有人可读的密钥类文件。权限显示为 -rw-r--r-- 的 .env,意味着服务器上任何账号、任何被攻破的进程都能读到它。还要确认 .env 不在网站根目录下(例如 /var/www/html/.env),否则可能直接被人从浏览器下载。

这一步不能说明:密钥有没有已经被别人看到或拷走。它只告诉你密钥现在放在哪、谁能读。

2. 代码仓库:现在的文件和全部历史

在服务器上部署用的仓库目录里,以及你自己电脑上的仓库里,都做一遍。

【只读】 哪些敏感文件被 Git 跟踪了:

cd /path/to/app
git ls-files | grep -Ei '(^|/)\.env($|\.)|\.pem$|\.key$|id_rsa|id_ed25519|credentials|\.p12$'
git check-ignore -v .env || echo ".env 没有被 .gitignore 忽略"

【只读】 用扫描工具查当前文件和全部历史(推荐):

gitleaks git --redact -v .
gitleaks dir --redact -v .
trufflehog git file://. --no-verification

【只读】 不装工具时,用 git log 粗查:

git log --all -i -G '(password|secret|token|api_?key)[[:space:]]*[:=]' --format='%h %ad %an %s' --date=short
git log --all --format='%h %ad %s' --date=short -- .env '*.pem' '*.key'
git show --stat COMMIT_ID

如果仓库在 GitHub 等平台上,同时看一下平台自带的密钥扫描提醒,以及流水线(CI)的变量和日志里有没有打印过密钥。

3. 处置:先换新的,再删旧的

第 1、2 步找到的每一个密钥,都按这个顺序处理。本步的命令除特别标注外都是 【改动】。

第一件事:更换(轮换)密钥。 在服务商或数据库里生成新的密码或 token,填到应用用它的地方,重启应用,确认功能正常,最后停用旧的。旧的停用之前,随时可以切回去。

第二件事:收紧权限。 先记下原来的权限,方便回退:

stat -c '%a %U:%G %n' /path/to/app/.env
sudo chown APP_USER:APP_USER /path/to/app/.env
sudo chmod 600 /path/to/app/.env

第三件事:把密钥从会进仓库、会被人看到的地方挪走。

最后(可选):清理 Git 历史。 密钥都换过之后,可以用 git filter-repo 把敏感文件从历史里移除。这一步会改写全部提交号,需要强制推送,所有协作者要重新克隆,做之前先备份整个仓库。它是清理,不是补救:做不做都必须先完成更换。 具体做法见文末 GitHub 的官方说明。

把日志和终端输出发给 AI 之前,先脱敏

不管用哪个 AI 工具,发出去的日志和命令输出就离开了你的服务器。排查时最常带出密钥的是:错误日志里的数据库连接串、开了调试模式打出的请求头、env / printenv / docker inspect 的输出,以及 set -x 的脚本日志。

要去掉的内容:

【只读,另存一份新文件,不改原文件】 一个 sed 例子,把上面几类替换成占位:

sed -E \
  -e '/-----BEGIN [A-Z ]*PRIVATE KEY-----/,/-----END [A-Z ]*PRIVATE KEY-----/c [REDACTED_PRIVATE_KEY]' \
  -e 's#(://[^:/@[:space:]]+:)[^@[:space:]]+@#\1[REDACTED]@#g' \
  -e 's/(Authorization:[[:space:]]*(Bearer|Basic|Token)[[:space:]]+)[^[:space:]]+/\1[REDACTED]/Ig' \
  -e 's/((password|passwd|secret|token|api[_-]?key|access[_-]?key)[A-Za-z0-9_]*"?[[:space:]]*[:=][[:space:]]*"?)[^",}[:space:]]+/\1[REDACTED]/Ig' \
  -e 's/eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/[REDACTED_JWT]/g' \
  -e 's/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/[EMAIL]/g' \
  -e 's/\b([0-9]{1,3}\.){3}[0-9]{1,3}\b/[IP]/g' \
  app-error.log > app-error.redacted.log

效果示例:DB_PASSWORD=CHANGE_ME_EXAMPLE 变成 DB_PASSWORD=[REDACTED],postgres://app:EXAMPLE_PASSWORD@db.example.com/app 变成 postgres://app:[REDACTED]@db.example.com/app。这是 GNU sed 的写法(常见 Linux 发行版自带);把最后一行换成管道也可以,例如 docker logs --since 30m YOUR_CONTAINER 2>&1 | sed -E …。

脱敏完再自查一遍,下面这条没有输出才算干净:

grep -nEi 'password|secret|token|api_?key|BEGIN .*PRIVATE KEY|eyJ' app-error.redacted.log | grep -v REDACTED

正则只能兜底,不能保证一个不漏:自定义格式的密钥、写在别处的客户信息它都认不出。最可靠的办法是一开始就用第 1 步那种“只显示名字”的命令,不让值出现在屏幕上。

如果你用的是 OpsMate:每次让 AI 帮忙看,终端里最近的输出都会一起发到云端 AI,敏感信息请先去掉。

注意:clear 不会清掉发给 AI 的内容,要清就断开再重连终端。常见格式的密钥在发送前会被自动替换,但不能只靠它。最好的办法是一开始就别把密钥打在终端里。

在 OpsMate 里怎么做

OpsMate(itops.sh)把 SSH 终端和 AI 放在同一个服务器页面,本文的命令都可以直接在终端里手敲。未通过安全检查的命令会被拦截且不会执行。低风险修复默认自动执行,每一步都会记录。每次让 AI 帮忙看,终端里最近的输出都会一起发到云端 AI,敏感信息请先去掉。OpsMate 自己会做什么、不会做什么,以官网 常见问题 为准。

说明性示例

说明性示例(不是真实客户案例):一个三人团队的 VPS 上跑着 Node 应用。第 1 步查到 /srv/app/.env 权限是 644,Shell 历史里有两行 mysql -p…,compose 文件里有 1 行直接写的 POSTGRES_PASSWORD。第 2 步 gitleaks git 报出一年前的一个提交里有 .env,后来已经删除。团队先在数据库里给应用账号设新密码、更新 .env 和 compose 引用、重启、下单测试;再在邮件服务商后台生成新 API 密钥替换旧的,最后停用旧密钥。然后把 .env 改成 600、属主改成应用账号,删掉 Shell 历史里那两行。历史里那个旧 .env 里的密钥都已失效,团队决定暂不改写 Git 历史。这些不能证明:旧密钥在失效之前没有被别人用过。要查有没有被滥用,得看数据库和服务商那边的访问记录。

AI 排查提示词

我在自查这台服务器和 /path/to/app 仓库里有没有明文密钥。请先只做检查、不做任何改动:列出 .env 和配置文件、Shell 历史、Docker 容器环境变量、compose 文件、crontab、systemd 服务里可能含密码、token、API 密钥的位置,以及这些文件的权限和属主;再检查 Git 是否跟踪了 .env、私钥这类文件,历史里有没有出现过。只输出文件路径、行号和配置项名字,绝不输出任何值。不要修改、移动或删除文件,不要改权限,不要执行改写 Git 历史的命令。如果需要处置,只把建议步骤、影响和回退方法列给我,由我自己执行。

常见问题

密钥不小心提交到 GitHub 了,删掉那次提交够吗?

不够。只要仓库公开过、被别人克隆过,或者推送到平台后有人看过,就要当作已经泄露:先在服务商那边更换密钥,再考虑清理历史。删提交、改写历史只能防止以后有人在仓库里看到它,不能让已经被拷走的旧密钥失效。

密钥放在服务器的 .env 文件里安全吗?

对小团队来说是常见做法,前提是:权限为 600、属主是运行应用的账号、不在网站根目录下、不进 Git 仓库。团队变大、服务变多时,再考虑云厂商的密钥管理服务。权限怎么查、怎么改,见本文第 1 步和第 3 步。

把日志发给 AI 帮忙排查之前,要去掉哪些信息?

至少去掉密码、token、API 密钥、连接串里的密码、Authorization 请求头、私钥,以及客户邮箱、手机号、内网地址。可以用本文的 sed 命令先替换一遍,再人工过一眼。

试用

每月免费 500 次 AI 调用,服务器数量不限。桌面端默认把 SSH 凭证保留在本机。

参考

相关指南

需要帮助理解排查结果?

OpsMate(itops.sh)帮助开发者和运维人员用 AI 辅助排查,由你核对证据。每次让 AI 帮忙看,终端里最近的输出都会一起发到云端 AI,敏感信息请先去掉。

免费开始 本地凭证桌面端

排障指南