Files
affiliate_dash/backend/cmd/reencrypt_secrets/main.go
T
yml2213 d821fe13df fix(security): 分离 JWT 与数据加密密钥并新增启动安全体检
.env 中 JWT_SECRET 仍逐字节等于 .env.example 的示例值,且 DATA_ENCRYPTION_KEY
未设置、按 config.go 的回退逻辑实际等于 JWT_SECRET。任何见过仓库的人都能伪造
管理员 JWT,并解密库中全部 API Client / 回调密钥密文。

- JWT_SECRET 与 DATA_ENCRYPTION_KEY 改为各自独立的强随机值
- 新增 Config.SecurityWarnings():示例密钥 / 未分离 / release 下开调试日志均告警
- 新增 cmd/reencrypt_secrets:默认只预演,-apply 才写库;幂等,坏数据不中断整轮
- 新增 docs/密钥轮换说明.md;.gitignore 忽略 .env.bak*
- release 模式下关闭 OPEN_API_DEBUG
2026-10-06 19:42:42 +08:00

92 lines
2.6 KiB
Go

// Command reencrypt_secrets 在轮换 DATA_ENCRYPTION_KEY 时,
// 把存量以密文落库的 API Client / 回调密钥从旧密钥重新加密为新密钥。
//
// 典型用法:
//
// OLD_DATA_ENCRYPTION_KEY=<旧密钥> go run ./cmd/reencrypt_secrets # 预演
// OLD_DATA_ENCRYPTION_KEY=<旧密钥> go run ./cmd/reencrypt_secrets -apply # 真正写库
//
// 详见 docs/密钥轮换说明.md
package main
import (
"flag"
"fmt"
"log"
"os"
"affiliate_dash/internal/config"
"affiliate_dash/internal/service"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
func main() {
apply := flag.Bool("apply", false, "真正写库;不加该参数时只做预演(dry-run),不修改任何数据")
flag.Parse()
cfg := config.Load()
newKey := cfg.DataEncryptionKey
// 兼容两种历史配置:显式配置过 DATA_ENCRYPTION_KEY,或曾经回退到 JWT_SECRET。
oldKey := os.Getenv("OLD_DATA_ENCRYPTION_KEY")
if oldKey == "" {
oldKey = os.Getenv("OLD_JWT_SECRET")
}
if oldKey == "" {
log.Fatal("缺少旧密钥:请用 OLD_DATA_ENCRYPTION_KEY(或旧 JWT_SECRET)环境变量提供轮换前的密钥")
}
if oldKey == newKey {
log.Fatal("新旧密钥相同,无需轮换")
}
oldCodec, err := service.NewSecretCodec(oldKey)
if err != nil {
log.Fatalf("初始化旧密钥编解码器失败: %v", err)
}
newCodec, err := service.NewSecretCodec(newKey)
if err != nil {
log.Fatalf("初始化新密钥编解码器失败: %v", err)
}
db, err := gorm.Open(postgres.Open(cfg.DatabaseURL), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
log.Fatalf("连接数据库失败: %v", err)
}
sqlDB, err := db.DB()
if err != nil {
log.Fatalf("获取数据库连接失败: %v", err)
}
defer sqlDB.Close()
mode := "预演(dry-run)"
if *apply {
mode = "写入(apply)"
}
fmt.Printf("密钥轮换模式: %s\n\n", mode)
results, err := service.ReencryptSecrets(db, oldCodec, newCodec, *apply)
if err != nil {
log.Fatalf("重加密失败: %v", err)
}
for _, r := range results {
fmt.Printf("%-24s 扫描=%d 重加密=%d 已是新密钥=%d 失败=%d\n",
r.Table, r.Report.Scanned, r.Report.Reencrypted, r.Report.AlreadyNewKey, r.Report.Failed)
}
if failed := service.TotalFailed(results); failed > 0 {
log.Fatalf("有 %d 行新旧密钥都无法解密,请先排查数据来源,未做任何破坏性操作", failed)
}
if !*apply {
fmt.Println("\n预演完成,未修改任何数据。确认无误后加 -apply 执行写入。")
return
}
fmt.Println("\n重加密完成。请确认业务正常后再移除旧密钥。")
}