先看三件真实发生的事:
开发者把 AWS Access Key 提交到公开 GitHub 仓库,几分钟内自动扫描脚本捕获密钥,攻击者启动加密货币挖矿实例,账单五位数美元。流行 npm 包的维护者账户被盗,攻击者在更新中植入窃取环境变量的恶意代码,所有下游项目全部中招。员工把生产数据库密码写死在代码里,离职后账号被禁用,但密码仍在仓库历史里,三个月后才被发现。
这三件事有一个共同点:都不是”高级攻击”,都是基础习惯缺失。安全不是安全团队的事,是每个开发者写代码时的日常底线。
1. 敏感信息不入库
什么不能进 Git
以下内容永远不应该出现在 Git 仓库,无论是公开还是私有:
| 类型 | 示例 |
|---|---|
| API 密钥 | AWS Access Key、Stripe Secret Key |
| 数据库密码 | mysql://root:password@prod-db:3306/app |
| Token | GitHub PAT、Slack Bot Token |
| 证书私钥 | -----BEGIN RSA PRIVATE KEY----- |
| JWT Secret | 用于签发 JWT 的密钥 |
| 云服务凭证 | ~/.aws/credentials、gcloud 认证文件 |
Git 历史是永久记录
“把敏感文件删掉再提交就行了”——不行。Git 把所有历史版本都永久保存,git log 和 git 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 失败代价低得多。
泄露后的应急四步
一旦密钥进了仓库(无论是否已推送),按这个顺序处理:
- 立即轮换密钥——撤销旧密钥,生成新密钥。这比清理历史更紧急,也更重要
- 清理 Git 历史——用 BFG Repo-Cleaner 重写历史,删除包含密钥的提交
- 审计日志——检查泄露期间是否有异常访问、是否已被利用
- 复盘——分析根因,补上防线
公开仓库即使清理了历史,也应认为密钥已泄露,必须轮换。历史可以删,但已经被爬取的记录删不掉。
2. 密钥与凭证管理
存储方式按场景选
| 密钥类型 | 推荐存储 | 注意 |
|---|---|---|
| 云服务 API Key | 环境变量 + IAM 权限限制 | 不给 Admin 权限 |
| SSH 私钥 | passphrase 保护 + ssh-agent | 见 01-SSH.md |
| 数据库密码 | 环境变量(开发)/ Secrets Manager(生产) | 生产环境不够用环境变量 |
| JWT Secret | Vault / 加密存储 | 定期轮换,最少 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>/members4. 加密工具选型
开发者日常加密场景归为三类:加密文件内容、管理项目密钥、证明提交身份。工具各有定位,不要混用。
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 trueGPG 签名(生态更成熟,开源项目和企业合规通常要求):
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 Manager | 高 | AWS 生态 |
| HashiCorp Vault | 很高 | 企业级 |
5. 身份认证强化
密码管理器
不要重复使用密码。一个服务被攻破,攻击者会用你的邮箱和密码尝试登录所有其他服务——这叫凭证填充攻击,自动化且高效。
使用密码管理器:1Password(付费,体验好)、Bitwarden(开源免费,可自托管)、KeePassXC(完全本地)。你只需要记一个主密码。
双因素认证(2FA)
2FA 需要两样东西:你知道的(密码)+ 你拥有的(验证码 / 硬件密钥)。即使密码泄露,没有第二因素也无法登录。
| 方式 | 说明 | 优劣 |
|---|---|---|
| TOTP | Google 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,高危漏洞即失败 |
| Dependabot | GitHub 自动提 PR 更新有漏洞的依赖 |
| 安装前审查 | 看维护者、star 数、最后更新时间 |
7. 社会工程防范
社会工程攻击不针对技术漏洞,针对人。技术再安全,如果人被骗了,防护形同虚设。
钓鱼邮件:伪装成 GitHub Security Alert,发件域名是 github-alerts.com,要求 24 小时内点链接修复”高危漏洞”。防范:不点邮件中的链接,自己去 github.com 查看安全通知。
Typosquatting:npm install colour 而非 colors,视觉几乎一样的包名。防范:从官方文档复制包名,不从陌生来源复制安装命令。
假招聘面试:要求下载”技术测试”项目并 npm install,postinstall 脚本窃取 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 和密码管理器是基础设施:不是”高级安全配置”,是开发者的日常身份边界,没有理由不用。
安全问题要留证据和路径:发现异常登录、密钥暴露、供应链风险时,记录时间、命令、日志和处理动作,方便复盘和向上汇报。