正确做法是k8s secret挂载为文件并设权限0440,go用os.readfile读取;敏感字段单独aes-gcm加密,nonce随机12字节,密钥严禁硬编码;结构体字段需主动脱敏并实现fmt.formatter和slog.logvalue。

配置文件里存密码、密钥、Token,不加密就提交 Git 或部署到服务器,等于把门禁卡贴在玻璃门上——能跑通,但一查日志、一扒进程、一翻镜像层,全暴露。
用 os.ReadFile 读取挂载的密钥文件,而不是 os.Getenv
环境变量是 Linux 下最松散的“共享内存”,ps aux、/proc/[pid]/environ、容器 env 命令全都能直接看到。Kubernetes 中用 envFrom 注入 Secret 更是让所有同用户进程共享明文。
- 正确做法:K8s Secret 挂载为文件(如
/etc/secrets/db/password),权限设为0440,Go 用os.ReadFile读取 - 本地开发时,用相同路径(如
./secrets/db/password)模拟,由make secrets生成,不进 Git - 绝不能打印读取结果长度或内容本身——
log.Printf("pwd len: %d", len(b))也可能构成侧信道 - 若必须用环境变量,至少用
os.LookupEnv显式校验:if pwd, ok := os.LookupEnv("DB_PASSWORD"); !ok || len(pwd) == 0
只加密敏感字段,别碰整配置文件
整文件 AES 加密会让 jq、helm template、yq 全失效,diff 看不出变更,CI 脚本要额外加解密步骤,还容易误把密文当明文提交。
- 推荐方案:TOML/YAML/JSON 保持明文,仅对特定键加密,如
database.password、api.token - 解析后递归遍历 map[string]interface{},命中敏感 key 名时调用解密函数还原为字符串
-
viper不支持运行时修改嵌套结构,建议先用mapstructure.Decode转成 map 再处理 - YAML 的
!vault标签只是伪加密——本质还是 base64 编码的明文,别被名字骗了
用 crypto/aes + GCM 加密,别手搓 CBC+HMAC
Go 标准库没有 encryptConfigFile 这种函数,所有加解密逻辑都得自己写。AES-GCM 是目前最稳妥的选择:一次调用同时完成加密和认证,避免 IV 重用、HMAC 验证顺序等经典坑。
- nonce 必须随机生成且固定 12 字节:
nonce := make([]byte, 12); rand.Read(nonce),绝不能用时间戳或计数器 - 密文存储格式为
nonce + ciphertext + authTag,解密时先拆出前 12 字节作 nonce - 密钥严禁硬编码:从环境变量(仅路径)、KMS(Vault/AWS KMS)或文件(
chmod 600)加载 - 解密失败统一返回
400 Bad Request,不暴露是密钥错、nonce 错还是数据损坏
结构体字段必须主动脱敏,unexported 不等于安全
小写字段(如 password string)在本包内 fmt.Println 照样输出明文;反射、unsafe、日志库底层也能读取。真正防泄露得靠接口拦截。
- 对敏感字段类型实现
fmt.Formatter,让所有fmt输出固定为"" - 若用
log/slog(Go 1.21+),还需实现slog.LogValue方法,否则日志仍会打明文 - 结构体定义中加
json:"-" yaml:"-"标签,防止自动序列化泄漏 - 禁止在 HTTP handler 中传整个配置 struct 给
slog.With,必须手动提取非敏感字段构造键值对
最容易被跳过的其实是密钥生命周期管理:nonce 重复用一次,GCM 就崩;密钥进了 swap 分区,内存 dump 就能复原;哪怕只在调试时 fmt.Printf("%+v", cfg) 一次,日志系统就可能永久存档。安全不是加个 AES,而是让敏感值在内存里待得足够短、路径足够窄、出口足够少。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











