3. 密钥存储不当:文件系统权限不足、数据库明文存储、备份泄露风险
密钥存储,说白了就是你把家底藏在哪的问题。我见过太多团队,加密算法选得挺强,密钥长度也够,结果密钥就明文躺在配置文件里——那感觉就像给金库装了最好的锁,然后把钥匙挂在门口。
这一章我们专门聊聊密钥存储的三大坑:文件系统权限失控、数据库里存明文、备份泄露。每个我都踩过,或者看别人踩过。
3.1 文件系统权限不足
很多应用启动时需要读取密钥文件。如果权限设成 644 甚至 777,那任何能登录服务器的用户都能把密钥拷走。
我记得有一次做安全审计,发现某项目的私钥文件权限是 755。问运维为什么,他说“这样方便备份脚本读取”。嗯,方便是方便了,攻击者也方便了。
- 密钥文件与代码放在同一目录,权限继承自项目目录(通常是 755)
- 使用 root 用户启动应用,密钥文件属主为 root,但权限设为 644
- 将密钥文件放在 Web 可访问的静态资源目录下
正确的做法是什么?我个人的习惯是:
- 密钥文件属主设为启动应用的服务账户(如
appuser) - 权限设为 400(仅属主可读)或 600(属主可读写)
- 密钥文件放在应用代码目录之外,比如
/etc/secrets/
# 创建专用目录
sudo mkdir -p /etc/secrets
sudo chmod 750 /etc/secrets
# 存放密钥文件
sudo cp private_key.pem /etc/secrets/
sudo chown appuser:appgroup /etc/secrets/private_key.pem
sudo chmod 400 /etc/secrets/private_key.pem
可以用 ls -la 定期扫描所有密钥文件的权限。我曾经写过一个简单的 cron 脚本,每天检查一次,发现权限异常就告警。这招帮我抓到了两次运维误操作。
3.2 数据库明文存储
这个坑更常见。业务需要加密某些字段,结果开发同学直接把密钥存到了数据库的配置表里,还是明文。
你想想看,数据库一旦被拖库,攻击者拿到的不只是数据,连解密的钥匙都一并奉上了。这叫什么?这叫买一送一。
我遇到过最离谱的一次,是某金融项目把支付接口的 RSA 私钥存在 MySQL 的 system_config 表里,字段名叫 payment_private_key,值就是 PEM 格式的明文。我当时差点没背过气去。
密钥永远不应该与应用数据存储在同一个数据库中。如果必须存,至少要做到:
- 使用数据库的加密功能(如 MySQL 的 AES_ENCRYPT)
- 加密密钥的密钥(KEK)从外部系统获取,不落库
- 对密钥表的访问权限做严格限制
更好的方案是使用专门的密钥管理服务(KMS),比如 AWS KMS、Azure Key Vault 或 HashiCorp Vault。应用运行时只从这些服务获取密钥,不持久化到本地。
| 存储方式 | 风险等级 | 说明 |
|---|---|---|
| 数据库明文存储 | 🔴 极高 | 拖库即泄露,毫无防护 |
| 数据库加密存储 | 🟡 中等 | 需要额外管理 KEK,增加复杂度 |
| 专用密钥管理服务 | 🟢 低 | 业界最佳实践,但依赖外部服务 |
| 硬件安全模块(HSM) | 🟢 极低 | 物理隔离,成本高 |
3.3 备份泄露风险
这个点容易被忽略。很多公司对生产环境的密钥管控很严,但备份呢?
我曾经参与过一次事后复盘:某公司服务器硬盘坏了,运维从备份恢复数据。结果发现备份文件里包含了所有密钥,而且备份文件本身没有加密,存储在 AWS S3 上,权限还是公开可读的。这个备份在 S3 上躺了两年,谁也不知道有没有被人下载过。
为什么会这样?因为备份脚本通常由运维编写,他们关注的是数据完整性,很少考虑密钥是否被一并备份了。
- 全量备份包含密钥文件
- 备份文件未加密
- 备份存储权限配置错误
- 备份介质丢失(如磁带、硬盘)
- 备份传输过程未使用加密通道
修复方案其实不复杂:
- 备份前排除密钥文件:在备份脚本中明确排除
/etc/secrets/等目录 - 单独备份密钥:密钥使用独立的备份策略,加密后存储
- 备份文件本身加密:使用 GPG 或类似工具对备份文件加密
- 定期验证备份恢复:确保恢复流程中密钥能正确还原
# 备份脚本示例:排除密钥目录
tar czf /backup/app_$(date +%Y%m%d).tar.gz \
--exclude=/etc/secrets \
/var/www/app
# 单独加密备份密钥
gpg --encrypt --recipient backup@company.com \
/etc/secrets/private_key.pem
3.4 修复方案:加密存储 + 访问控制
讲完了问题,我们聊聊怎么修。核心就两招:加密存储和访问控制。
3.4.1 加密存储
密钥本身也要加密。听起来有点绕,但这是分层加密的基础思想。
我习惯的做法是:
- 使用主密钥(Master Key)加密所有业务密钥
- 主密钥存储在 HSM 或 KMS 中
- 业务密钥加密后存储在文件系统或数据库中
- 应用启动时,先从 KMS 获取主密钥,再用主密钥解密业务密钥
主密钥 → 加密 → 密钥加密密钥(KEK)→ 加密 → 数据加密密钥(DEK)→ 加密 → 业务数据
这样即使业务密钥文件泄露,攻击者没有主密钥也无法解密。
# Python 示例:使用 KMS 加密本地密钥
import boto3
from cryptography.fernet import Fernet
kms = boto3.client('kms')
# 生成数据密钥
response = kms.generate_data_key(
KeyId='alias/my-master-key',
KeySpec='AES_256'
)
# 明文数据密钥(仅内存中使用)
plaintext_key = response['Plaintext']
# 加密后的数据密钥(可安全存储)
encrypted_key = response['CiphertextBlob']
# 使用数据密钥加密业务密钥
fernet = Fernet(plaintext_key)
encrypted_business_key = fernet.encrypt(business_key_bytes)
# 存储 encrypted_key 和 encrypted_business_key
# 丢弃 plaintext_key
3.4.2 访问控制
加密做得再好,如果谁都能读到密钥文件,那也是白搭。
访问控制要覆盖三个层面:
| 层面 | 措施 | 说明 |
|---|---|---|
| 操作系统 | 文件权限、SELinux、AppArmor | 限制哪些进程可以读取密钥文件 |
| 应用层 | 最小权限原则、密钥轮换 | 每个服务只拥有它需要的密钥 |
| 网络层 | KMS 访问策略、VPC 端点 | 限制密钥管理服务的网络访问来源 |
我个人比较推崇“零信任”的思路:不信任任何网络、任何进程、任何用户。每次访问密钥都要显式授权,并且记录审计日志。
我曾经遇到过一个问题:开发环境为了方便,把密钥权限放得很宽。结果测试同学写了个自动化脚本,不小心把密钥文件上传到了公开的 GitHub 仓库。从那以后,我坚持所有环境(包括开发环境)的密钥权限必须和生产环境一致。麻烦是麻烦了点,但安全没有捷径。
3.5 知识体系总览
下面这张图总结了密钥存储不当的核心问题与修复路径,你可以把它当作本章的思维导图:
这张图把三个漏洞和修复方案串在了一起。你可以看到,文件系统、数据库、备份这三个入口,最终都指向同一个修复方向:加密存储 + 访问控制。
好了,这一章的内容就到这里。密钥存储是密钥管理的基础,基础打不牢,上层建筑再漂亮也没用。希望你能把这些原则落实到实际项目中,别让密钥成为你系统的短板。