Gin 配置文件加密与安全
配置文件安全是应用安全的重要环节,敏感信息泄露可能导致严重安全事故。
安全原则
Go
1. 敏感信息禁止硬编码在代码中
2. 配置文件中敏感信息需加密
3. 使用环境变量传递密钥
4. 配置文件权限严格控制
5. 密钥定期轮换
敏感信息识别
需加密的配置项
YAML
- 数据库密码
- Redis 密码
- API 密钥
- JWT 密钥
- 加密密钥
- 第三方服务凭证
- SMTP 密码
- OAuth Token
AES 加密配置
加密工具实现
Go
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
)
type ConfigEncryptor struct {
key []byte // 32 bytes for AES-256
}
func NewConfigEncryptor(key string) *ConfigEncryptor {
// 确保密钥长度为 32 字节
keyBytes := []byte(key)
if len(keyBytes) < 32 {
// 填充到 32 字节
padded := make([]byte, 32)
copy(padded, keyBytes)
keyBytes = padded
}
return &ConfigEncryptor{key: keyBytes[:32]}
}
func (ce *ConfigEncryptor) Encrypt(plaintext string) (string, error) {
block, err := aes.NewCipher(ce.key)
if err != nil {
return "", err
}
// GCM 模式
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
// 生成 nonce
nonce := make([]byte, gcm.NonceSize())
rand.Read(nonce)
// 加密
ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
// Base64 编码
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
func (ce *ConfigEncryptor) Decrypt(ciphertext string) (string, error) {
block, err := aes.NewCipher(ce.key)
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
// Base64 解码
data, err := base64.StdEncoding.DecodeString(ciphertext)
if err != nil {
return "", err
}
nonceSize := gcm.NonceSize()
nonce, cipherData := data[:nonceSize], data[nonceSize:]
// 解密
plaintext, err := gcm.Open(nil, nonce, cipherData, nil)
if err != nil {
return "", err
}
return string(plaintext), nil
}
配置文件加密格式
Go
# config/production.yaml
database:
host: db.internal.example.com
port: 3306
user: gin_user
password_encrypted: ENC:aes256:GCM:base64encodedvalue
name: gin_prod
redis:
host: redis.internal.example.com
port: 6379
password_encrypted: ENC:aes256:GCM:base64encodedvalue
jwt:
secret_encrypted: ENC:aes256:GCM:base64encodedvalue
加密配置加载
YAML
func LoadEncryptedConfig() (*Config, error) {
// 从环境变量获取加密密钥
encryptKey := os.Getenv("CONFIG_ENCRYPT_KEY")
if encryptKey == "" {
return nil, errors.New("CONFIG_ENCRYPT_KEY not set")
}
encryptor := NewConfigEncryptor(encryptKey)
v := viper.New()
v.SetConfigName("production")
v.SetConfigType("yaml")
v.AddConfigPath("config")
v.ReadInConfig()
var config Config
v.Unmarshal(&config)
// 解密敏感字段
if config.Database.PasswordEncrypted != "" {
decrypted, err := encryptor.Decrypt(config.Database.PasswordEncrypted)
if err != nil {
return nil, err
}
config.Database.Password = decrypted
}
if config.Redis.PasswordEncrypted != "" {
decrypted, err := encryptor.Decrypt(config.Redis.PasswordEncrypted)
if err != nil {
return nil, err
}
config.Redis.Password = decrypted
}
if config.JWT.SecretEncrypted != "" {
decrypted, err := encryptor.Decrypt(config.JWT.SecretEncrypted)
if err != nil {
return nil, err
}
config.JWT.Secret = decrypted
}
return &config, nil
}
环境变量安全
使用环境变量
YAML
// 优先环境变量,其次配置文件
func GetSecret(key string, configKey string) string {
// 优先从环境变量获取
value := os.Getenv(key)
if value != "" {
return value
}
// 其次从加密配置获取
encryptedValue := viper.GetString(configKey + "_encrypted")
if encryptedValue != "" {
decryptor := NewConfigEncryptor(os.Getenv("CONFIG_ENCRYPT_KEY"))
decrypted, _ := decryptor.Decrypt(encryptedValue)
return decrypted
}
return ""
}
func main() {
dbPassword := GetSecret("DB_PASSWORD", "database.password")
redisPassword := GetSecret("REDIS_PASSWORD", "redis.password")
jwtSecret := GetSecret("JWT_SECRET", "jwt.secret")
}
Kubernetes Secret
Go
# k8s-secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: gin-api-secrets
type: Opaque
data:
db-password: base64encodedvalue
redis-password: base64encodedvalue
jwt-secret: base64encodedvalue
dockerfile
# k8s-deployment.yaml
apiVersion: apps/v1
kind: Deployment
spec:
containers:
- name: gin-api
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: gin-api-secrets
key: db-password
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: gin-api-secrets
key: redis-password
配置文件权限
文件权限设置
Go
func SecureConfigFiles(configDir string) error {
// 设置配置目录权限:仅所有者可读写执行
os.Chmod(configDir, 0700)
// 设置配置文件权限:仅所有者可读写
files, _ := os.ReadDir(configDir)
for _, f := range files {
if strings.HasSuffix(f.Name(), ".yaml") ||
strings.HasSuffix(f.Name(), ".yml") ||
strings.HasSuffix(f.Name(), ".json") {
os.Chmod(filepath.Join(configDir, f.Name()), 0600)
}
}
return nil
}
func main() {
SecureConfigFiles("config")
}
Docker 配置安全
Go
# Dockerfile
COPY config/*.yaml /app/config/
RUN chmod 700 /app/config && \
chmod 600 /app/config/*.yaml
# 使用非 root 用户
RUN adduser -D appuser
USER appuser
密钥轮换机制
密钥轮换实现
YAML
type KeyManager struct {
currentKey []byte
previousKey []byte
keyVersion int
}
func NewKeyManager(currentKey, previousKey string) *KeyManager {
return &KeyManager{
currentKey: []byte(padKey(currentKey)),
previousKey: []byte(padKey(previousKey)),
keyVersion: 1,
}
}
func (km *KeyManager) Decrypt(ciphertext string) (string, error) {
// 尝试当前密钥
encryptor := NewConfigEncryptor(string(km.currentKey))
plaintext, err := encryptor.Decrypt(ciphertext)
if err == nil {
return plaintext, nil
}
// 尝试旧密钥(支持密钥轮换过渡)
if km.previousKey != nil {
encryptor = NewConfigEncryptor(string(km.previousKey))
plaintext, err = encryptor.Decrypt(ciphertext)
if err == nil {
// 使用旧密钥成功,提示需要重新加密
return plaintext, nil
}
}
return "", errors.New("decryption failed with all keys")
}
func (km *KeyManager) RotateKey(newKey string) {
km.previousKey = km.currentKey
km.currentKey = []byte(padKey(newKey))
km.keyVersion++
}
密钥轮换流程
Go
1. 生成新密钥
2. 使用新密钥重新加密所有配置
3. 更新环境变量/Secret
4. 部署新版本应用
5. 验证应用正常运行
6. 清理旧密钥
配置完整性验证
配置签名
Go
import (
"crypto/hmac"
"crypto/sha256"
)
type ConfigSigner struct {
signingKey []byte
}
func NewConfigSigner(key string) *ConfigSigner {
return &ConfigSigner{signingKey: []byte(key)}
}
func (cs *ConfigSigner) Sign(configData string) string {
h := hmac.New(sha256.New, cs.signingKey)
h.Write([]byte(configData))
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
func (cs *ConfigSigner) Verify(configData, signature string) bool {
expectedSig := cs.Sign(configData)
return hmac.Equal([]byte(expectedSig), []byte(signature))
}
带签名的配置文件
Go
# config/production.yaml
_config_signature: sha256:hmac:base64signature
database:
host: db.internal.example.com
password_encrypted: ENC:aes256:value
text
func LoadSignedConfig() (*Config, error) {
signingKey := os.Getenv("CONFIG_SIGNING_KEY")
signer := NewConfigSigner(signingKey)
data, err := os.ReadFile("config/production.yaml")
if err != nil {
return nil, err
}
// 验证签名
v := viper.New()
v.SetConfigType("yaml")
v.ReadConfig(bytes.NewReader(data))
signature := v.GetString("_config_signature")
configData := removeSignature(data)
if !signer.Verify(configData, signature) {
return nil, errors.New("config signature invalid")
}
// 加载配置
var config Config
v.Unmarshal(&config)
return &config, nil
}
安全审计
配置审计日志
text
func AuditConfigAccess(config *Config, operation string) {
log.Printf("Config access audit: %s by %s at %s",
operation,
getCurrentUser(),
time.Now().Format(time.RFC3339),
)
// 记录敏感字段访问(不记录值)
if config.Database.Password != "" {
log.Printf("Sensitive field accessed: database.password")
}
}
配置变更通知
text
func NotifyConfigChange(oldConfig, newConfig *Config) {
changes := []string{}
if oldConfig.Database.Host != newConfig.Database.Host {
changes = append(changes, "database.host changed")
}
if len(changes) > 0 {
// 发送通知(邮件、Slack等)
sendAlert(fmt.Sprintf("Config changed: %s", strings.Join(changes, ", ")))
}
}
安全检查清单
| 检查项 | 说明 |
|---|---|
| 密钥长度 | AES-256 需要 32 字节密钥 |
| 文件权限 | 600(仅所有者读写) |
| 环境变量 | 敏感信息使用环境变量 |
| 加密算法 | AES-GCM 推荐使用 |
| 密钥存储 | 专用密钥管理系统 |
| 变更日志 | 记录配置变更 |
注意:加密密钥禁止存储在代码仓库,必须通过安全渠道传递。
要点总结
- 敏感识别:密码、密钥、Token 等必须加密
- AES-GCM:推荐加密算法,支持完整性验证
- 环境变量:加密密钥通过环境变量注入
- 文件权限:配置文件权限 600,目录权限 700
- 密钥轮换:定期更换加密密钥
- 完整性:配置签名防止篡改
📝 发现内容有误?点击此处直接编辑