4. 本地密钥存储方案:文件系统、数据库与 Vault

聊完了密钥的生命周期管理,咱们得落地了。密钥存哪儿?这是个很现实的问题。我见过不少团队,一开始图省事,直接把密钥写死在配置文件里,结果上线第一天就被安全扫描工具扫出来,紧急回滚,狼狈得很。

今天咱们就掰扯清楚三种本地存储方案:文件系统存储、数据库加密存储,以及Vault。这三种方案,说白了就是「成本、复杂度、安全性」的三角博弈。你想想看,没有银弹,只有取舍。

4.1 文件系统存储:最朴素,也最容易翻车

文件系统存储,就是把密钥写到一个文件里,比如 /etc/join-server/secrets/ 目录下。嗯,这里要注意:千万别放项目根目录。我早期一个项目,把私钥放 Git 仓库里,虽然 .gitignore 了,但同事一不留神就 push 上去了,吓得我赶紧轮换所有凭证。

⚠️ 避坑指南:我曾经在客户现场排查过一起事故——运维把密钥文件权限设成了 644,结果被另一个低权限进程读走了,导致内部数据泄露。记住:密钥文件权限必须设为 600 或 400,属主只能是启动服务的专用用户。

文件系统存储的典型做法:

# 目录结构示例
/etc/join-server/secrets/
├── join-server.key          # 私钥,权限 600
├── join-server.crt          # 证书,权限 644
├── database-credentials.yml # 数据库凭证,权限 600
└── .env                     # 环境变量,权限 600

我个人习惯用 chmod 600 后,再用 chattr +i 加个不可修改属性,防止误删或篡改。但说实话,文件系统存储只适合单机部署或开发环境。生产环境一旦上了多节点,你就得考虑文件同步问题——那又是另一个坑。

维度 文件系统存储 数据库加密存储 Vault
部署复杂度 极低 中 高
安全性 低(依赖 OS 权限) 中(加密+访问控制) 高(动态密钥+审计)
密钥轮换 手动 半自动 全自动
审计能力 无 弱(数据库日志) 强(完整审计轨迹)
适用场景 开发/单机 中小规模集群 大规模/合规要求高

4.2 数据库加密存储:比文件系统强,但别裸存

把密钥存数据库,听起来比文件系统靠谱?其实不然。如果你直接把密钥明文存到 secrets 表里,那跟写在配置文件里没区别——DBA 一查表就全看见了。

正确的做法是:先加密,再存储。我个人推荐使用 AES-256-GCM 进行加密,因为 GCM 模式自带认证,能防篡改。

核心原则:数据库只存密文,加密密钥必须独立存储(比如放在文件系统或 Vault 里)。这叫「加密密钥与加密数据分离」——你想想看,如果数据库被拖库,攻击者拿到密文但没有加密密钥,也是白搭。

下面是一个 Go 语言的示例,展示如何加密后存入数据库:

import (
    "crypto/aes"
    "crypto/cipher"
    "crypto/rand"
    "encoding/base64"
    "io"
)

// 加密密钥,建议从环境变量或 Vault 获取
var encryptionKey = []byte("your-32-byte-aes-key-here!")

func Encrypt(plaintext []byte) (string, error) {
    block, err := aes.NewCipher(encryptionKey)
    if err != nil {
        return "", err
    }

    // GCM 模式
    gcm, err := cipher.NewGCM(block)
    if err != nil {
        return "", err
    }

    nonce := make([]byte, gcm.NonceSize())
    if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
        return "", err
    }

    ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
    return base64.StdEncoding.EncodeToString(ciphertext), nil
}

func Decrypt(ciphertextB64 string) ([]byte, error) {
    ciphertext, _ := base64.StdEncoding.DecodeString(ciphertextB64)
    block, _ := aes.NewCipher(encryptionKey)
    gcm, _ := cipher.NewGCM(block)

    nonceSize := gcm.NonceSize()
    nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]

    plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
    if err != nil {
        return nil, err // 数据被篡改或密钥错误
    }
    return plaintext, nil
}

💡 个人经验:我在项目中遇到过一个问题——加密密钥硬编码在代码里,结果代码被反编译了。后来我改用 KMS(密钥管理服务)来托管加密密钥,应用启动时通过 API 获取,内存中用完即销毁。虽然增加了网络开销,但安全性提升了一个量级。

数据库加密存储的典型表结构:

CREATE TABLE encrypted_secrets (
    id          SERIAL PRIMARY KEY,
    secret_name VARCHAR(255) NOT NULL UNIQUE,
    ciphertext  TEXT NOT NULL,          -- 加密后的密文
    key_id      VARCHAR(64) NOT NULL,   -- 加密密钥的标识,用于轮换
    created_at  TIMESTAMP DEFAULT NOW(),
    updated_at  TIMESTAMP DEFAULT NOW()
);

这里有个细节:key_id 字段很重要。当加密密钥轮换时,你可以根据 key_id 判断哪些记录需要重新加密。我曾经踩过这个坑——没有记录密钥版本,轮换后旧数据全解密失败,只能从备份恢复,折腾了一整夜。

4.3 使用 Vault 作为密钥存储后端

如果你问我生产环境推荐什么,我会毫不犹豫地说:Vault。HashiCorp Vault 是目前业界最成熟的密钥管理方案之一。它不只是存密钥,还提供动态密钥、租约、审计、自动轮换等能力。

Vault 的核心架构可以概括为「一锁、两器、三存储」:

  • 一锁:Seal/Unseal 机制,Vault 启动时处于密封状态,需要解封才能使用
  • 两器:Token 认证器 + 审计日志器
  • 三存储:后端存储(Consul/Raft/文件)、密钥存储、策略存储
Vault 密钥存储架构 Vault Server 密钥管理核心 认证层 Token / K8s / LDAP 密钥引擎 KV / PKI / Transit 审计层 Syslog / File 存储后端 Consul / Raft / 文件 解封机制 Shamir / Cloud KMS 说明: Vault 启动时处于密封状态,需要达到阈值数量的解封密钥才能解锁。 所有密钥操作都会记录审计日志,满足合规要求。

部署 Vault 的基本步骤:

  1. 初始化 Vault:生成解封密钥和根令牌
  2. 解封 Vault:使用 Shamir 密钥分片解封
  3. 启用密钥引擎:比如 vault secrets enable -path=join-server kv-v2
  4. 写入密钥:vault kv put join-server/config private_key=@./key.pem
  5. 配置访问策略:限制每个应用只能读取自己的路径

💡 实战建议:我个人习惯把 Vault 的 Unseal 密钥分片交给不同的人保管——比如运维负责人持 3 片,安全负责人持 2 片,架构师持 2 片。这样即使有人叛变了,也凑不够解封阈值。另外,生产环境建议用 Auto Unseal(比如 AWS KMS 或 Azure Key Vault),避免每次重启都要手动解封。

Vault 的 动态密钥 功能特别实用。比如数据库密码,传统做法是写死在配置里,泄露了就得手动改。Vault 可以动态生成数据库凭证,设置 TTL(比如 24 小时),到期自动失效。应用每次启动时从 Vault 获取临时凭证——就算泄露了,攻击者拿到的也是一张「过期票」。

# 配置 Vault 动态数据库凭证
vault write database/config/postgres \
    plugin_name=postgresql-database-plugin \
    allowed_roles="readonly" \
    connection_url="postgresql://{{username}}:{{password}}@localhost:5432/mydb" \
    username="vault_admin" \
    password="super_secret"

vault write database/roles/readonly \
    db_name=postgres \
    creation_statements="CREATE USER \"{{name}}\" WITH PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
    default_ttl="24h" \
    max_ttl="72h"

⚠️ 重要提醒:Vault 本身也会成为单点故障。我经历过一次 Vault 集群宕机,所有依赖 Vault 获取密钥的服务全部启动失败。所以,Vault 必须高可用部署(至少 3 节点),并且要有完善的备份和灾备方案。另外,应用层要做好降级策略——比如缓存密钥到本地文件,Vault 不可用时使用缓存。

4.4 三种方案如何选?

说了这么多,到底选哪个?我个人的经验是:

  • 开发环境 / 个人项目:文件系统存储就够了,别折腾。但记得加 .gitignore 和权限控制。
  • 中小规模生产环境(< 50 节点):数据库加密存储 + 独立的加密密钥管理。成本可控,安全性够用。
  • 大规模生产环境 / 合规要求高:直接上 Vault。虽然初期部署成本高,但长期来看,自动轮换、审计、动态密钥这些能力能省下大量运维精力。

最后说一句:没有绝对安全的方案,只有相对合适的选择。密钥存储的核心是「分层防御」——即使一层被攻破,还有另一层兜底。你想想看,如果攻击者既要攻破你的数据库,又要拿到加密密钥,还要绕过审计日志,那成本就高多了。