先看三件真实发生的事:

开发者把 AWS Access Key 提交到公开 GitHub 仓库,几分钟内自动扫描脚本捕获密钥,攻击者启动加密货币挖矿实例,账单五位数美元。流行 npm 包的维护者账户被盗,攻击者在更新中植入窃取环境变量的恶意代码,所有下游项目全部中招。员工把生产数据库密码写死在代码里,离职后账号被禁用,但密码仍在仓库历史里,三个月后才被发现。

这三件事有一个共同点:都不是”高级攻击”,都是基础习惯缺失。安全不是安全团队的事,是每个开发者写代码时的日常底线。

1. 敏感信息不入库

什么不能进 Git

以下内容永远不应该出现在 Git 仓库,无论是公开还是私有:

类型示例
API 密钥AWS Access Key、Stripe Secret Key
数据库密码mysql://root:password@prod-db:3306/app
TokenGitHub PAT、Slack Bot Token
证书私钥-----BEGIN RSA PRIVATE KEY-----
JWT Secret用于签发 JWT 的密钥
云服务凭证~/.aws/credentials、gcloud 认证文件

Git 历史是永久记录

“把敏感文件删掉再提交就行了”——不行。Git 把所有历史版本都永久保存,git loggit show 可以还原任何历史版本。GitHub 上有自动化扫描工具专门在 commit history 中搜索密钥。私有仓库也不安全:权限泄露或仓库设置改变后,历史里的密钥就暴露了。

预防

.gitignore 是第一道防线——在文件第一次被 git add 之前就设好:

.env
.env.local
.env.production
.env.*.local
*.pem
*.key
*.p12
.aws/credentials
secrets.yaml
config-private.*

提供 .env.example 模板(不含真实值,告诉协作者需要哪些变量):

# .env.example - 复制为 .env 并填入真实值
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
AWS_ACCESS_KEY_ID=your_access_key_here
JWT_SECRET=change_me_to_random_string_min_32_chars

用工具扫描,不靠人眼

brew install gitleaks
gitleaks detect --source . --verbose

把 gitleaks 加进 pre-commit hook,在本地拦截,比 CI 失败代价低得多。

泄露后的应急四步

一旦密钥进了仓库(无论是否已推送),按这个顺序处理:

  1. 立即轮换密钥——撤销旧密钥,生成新密钥。这比清理历史更紧急,也更重要
  2. 清理 Git 历史——用 BFG Repo-Cleaner 重写历史,删除包含密钥的提交
  3. 审计日志——检查泄露期间是否有异常访问、是否已被利用
  4. 复盘——分析根因,补上防线

公开仓库即使清理了历史,也应认为密钥已泄露,必须轮换。历史可以删,但已经被爬取的记录删不掉。

2. 密钥与凭证管理

存储方式按场景选

密钥类型推荐存储注意
云服务 API Key环境变量 + IAM 权限限制不给 Admin 权限
SSH 私钥passphrase 保护 + ssh-agent01-SSH.md
数据库密码环境变量(开发)/ Secrets Manager(生产)生产环境不够用环境变量
JWT SecretVault / 加密存储定期轮换,最少 32 字节
GitHub Token环境变量 + 最小权限必须设过期时间

环境变量 vs Secrets Manager

环境变量适合本地开发,但在生产环境有隐患:进程重启可能被日志捕获、ps aux 可能可见、容器编排日志可能泄露、无法做访问审计。

生产环境的标准是 Secrets Manager:

aws secretsmanager get-secret-value --secret-id prod/database/url
vault kv get -field=password secret/prod/database

优势:访问有审计日志、支持自动轮换、精细权限控制、密钥不出现在环境变量中。

Token 原则

创建 Token 时只勾选真正需要的权限(GitHub Token 只需读就给 repo:read,不给写);设置过期时间(GitHub 已强制要求);不创建”永不过期”的长期 Token。

3. 最小权限原则

最小权限原则(Principle of Least Privilege):每个账号、服务、程序只拥有完成当前任务所需的最小权限。多一分都不给。

数据库

-- 只给需要的操作,绝不给 DROP / ALTER / GRANT
GRANT SELECT, INSERT, UPDATE, DELETE ON mydb.* TO 'app_user'@'%';

云 IAM

{
  "Effect": "Allow",
  "Action": ["s3:GetObject", "s3:PutObject"],
  "Resource": "arn:aws:s3:::my-app-bucket/*"
}

只允许对特定 Bucket 的读写,不能删除 Bucket,不能访问其他资源,不能修改 IAM 策略。

Docker 容器

# 创建非 root 用户运行应用
FROM node:20
RUN groupadd -r appuser && useradd -r -g appuser appuser
COPY --chown=appuser:appuser . /app
USER appuser
CMD ["node", "server.js"]

不要用 --privileged 运行容器,那给了容器所有内核权限。

定期权限审计

每季度检查:谁还有什么权限?离职的员工账号是否已清理?IAM 有没有积累的过宽策略?服务账号是否还在使用?

aws iam list-users
aws iam list-attached-user-policies --user-name <username>
gh api orgs/<org>/members

4. 加密工具选型

开发者日常加密场景归为三类:加密文件内容、管理项目密钥、证明提交身份。工具各有定位,不要混用。

age — 文件加密

age 是 GPG 的现代替代品,设计哲学是只做一件事(文件加密)、做好它:

# 生成密钥对
age-keygen -o ~/.age/key.txt
 
# 用收件人公钥加密
age -r age1xxxxxxxxxx... secrets.txt > secrets.txt.age
 
# 密码加密(适合一次性分享)
age -p file.txt > file.txt.age
 
# 解密
age -d -i ~/.age/key.txt secrets.txt.age > secrets.txt

典型场景:加密备份文件、通过邮件或聊天发送敏感文件、与 chezmoi 集成加密 dotfiles。

sops — 配置文件密钥管理

sops 解决 age 无法解决的问题:加密配置文件中的敏感字段,同时保持文件结构可读

# sops 加密前
database:
  host: localhost        # 这行不加密
  password: my-secret   # 只加密 value
 
# sops 加密后
database:
  host: localhost
  password: ENC[AES256_GCM,data:xxx,iv:yyy,...]

这样 diff 可读、Code Review 可以看结构变化、不同环境的配置差异一目了然。团队成员各自提供 age 公钥,.sops.yaml 列出所有密钥,多人都可以解密。

适合把加密配置安全地放进 Git 版本控制的场景:K8s Secrets、Terraform variables、Helm values。

GPG / SSH 签名 — 提交签名

提交签名证明”这个 commit 确实是我做的”——GitHub 上的绿色 “Verified” 标记来源于此。没有签名,任何人都可以用你的邮箱和名字伪造提交。

SSH 签名(推荐,配置简单,复用现有密钥):

git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true

GPG 签名(生态更成熟,开源项目和企业合规通常要求):

gpg --gen-key
git config --global user.signingkey <KEY-ID>
git config --global commit.gpgsign true

密码管理器 CLI

环境变量不够安全时(见上节),密码管理器 CLI 是更好的方案:

# 1Password
export DB_PASS=$(op read "op://Private/MyDB/password")
 
# 或直接注入
op run --env-file .env.op -- node server.js
方案安全性适用
.env 文件低(不进 Git)本地开发
1Password CLI个人 / 小团队
AWS Secrets ManagerAWS 生态
HashiCorp Vault很高企业级

5. 身份认证强化

密码管理器

不要重复使用密码。一个服务被攻破,攻击者会用你的邮箱和密码尝试登录所有其他服务——这叫凭证填充攻击,自动化且高效。

使用密码管理器:1Password(付费,体验好)、Bitwarden(开源免费,可自托管)、KeePassXC(完全本地)。你只需要记一个主密码。

双因素认证(2FA)

2FA 需要两样东西:你知道的(密码)+ 你拥有的(验证码 / 硬件密钥)。即使密码泄露,没有第二因素也无法登录。

方式说明优劣
TOTPGoogle Authenticator / Authy,每 30 秒 6 位数字方便,但手机丢失麻烦
硬件密钥YubiKey / Google Titan Key最安全,无法被钓鱼,需要购买

必须开 2FA 的服务:GitHub / GitLab(代码仓库和 CI 密钥)、AWS / GCP / Azure(云资源和账单)、邮箱(密码重置入口,控制了邮箱等于控制了一切)、域名注册商(可以转移你的域名)。

GitHub 从 2023 年起已强制要求活跃贡献者开启 2FA。

6. 依赖安全

依赖树比你想象的深

npm install express 会拉下来几百个依赖包。你信任的不只是 express 的维护者,还有它依赖的包的维护者,以及那些包依赖的包……任何一个环节被攻破,你的项目就面临供应链风险。

三种风险形式

已知漏洞(CVE)

npm audit              # Node.js 依赖漏洞扫描
npm audit fix          # 自动修复
pip-audit              # Python

恶意包(Typosquatting):攻击者发布名字和流行包极相似的恶意包。reqeusts(字母调换)、col0rs(数字替换字母)、Iodash(大写 I 冒充小写 l)。防范:安装前仔细核对包名,看官方文档给出的包名,不要从论坛直接复制粘贴。

废弃包:长期无人维护,漏洞不会修复。用 npm view <pkg> time.modified 检查最后更新时间。

最佳实践

实践操作
锁定版本提交 package-lock.json / poetry.lock 到 Git
减少依赖能用标准库就不装包,每个依赖都是攻击面
自动化扫描CI 里跑 npm audit,高危漏洞即失败
DependabotGitHub 自动提 PR 更新有漏洞的依赖
安装前审查看维护者、star 数、最后更新时间

7. 社会工程防范

社会工程攻击不针对技术漏洞,针对人。技术再安全,如果人被骗了,防护形同虚设。

钓鱼邮件:伪装成 GitHub Security Alert,发件域名是 github-alerts.com,要求 24 小时内点链接修复”高危漏洞”。防范:不点邮件中的链接,自己去 github.com 查看安全通知。

Typosquattingnpm install colour 而非 colors,视觉几乎一样的包名。防范:从官方文档复制包名,不从陌生来源复制安装命令。

假招聘面试:要求下载”技术测试”项目并 npm installpostinstall 脚本窃取 SSH 密钥和 Token。防范:不要执行来路不明项目的安装命令,尤其是面试材料或陌生链接。

curl | bash

# 危险:不知道执行了什么
curl https://install.example.com/setup.sh | bash
 
# 安全:先看,再决定是否执行
curl -O https://install.example.com/setup.sh
cat setup.sh
chmod +x setup.sh && ./setup.sh

原则:太好的事情、太紧急的事情往往是攻击。有人要你”立刻”做某件安全相关的操作,先停一下,通过已知渠道确认身份。

8. 日常安全清单

频率检查项
每次 commit 前git diff 确认 diff 里没有 .env、密钥;新配置文件已加进 .gitignore
每次安装包前核对包名拼写,看维护者和更新时间
每周npm audit / pip-audit 扫依赖漏洞;处理 Dependabot PR
每月云 IAM 权限检查;SSH 密钥审查;2FA 状态确认
每季度轮换 GitHub Token 和 API Key;权限审查(谁有什么权限,是否还需要);密码管理器弱密码检查

9. 原则

密钥默认当作已泄露风险处理:一旦进入 Git 历史、聊天记录、截图或日志,优先撤销和轮换,不要只想着删除痕迹。

最小权限是默认姿势:数据库账号、云 IAM、Token、容器用户都只给当前任务需要的权限,不要因为”方便”给更多。

依赖安装前先看来源:陌生项目、面试题、脚本片段、curl | bash 都要先读再执行。

2FA 和密码管理器是基础设施:不是”高级安全配置”,是开发者的日常身份边界,没有理由不用。

安全问题要留证据和路径:发现异常登录、密钥暴露、供应链风险时,记录时间、命令、日志和处理动作,方便复盘和向上汇报。