Docker - 使用 Docker Secrets 敏感数据传输
Docker - 使用 Docker Secrets 敏感数据传输
Docker Secrets 是 Docker Swarm 提供的一种管理敏感数据的功能,用于安全地存储和传输密码、证书、私钥等不宜公开的数据,避免硬编码在镜像或配置文件中。
核心功能
- 数据加密:敏感数据在传输和存储时自动加密,仅限授权容器访问
- 权限控制:可精细控制哪些服务或任务能访问特定秘密
- 用途广泛:支持存储用户凭证、证书、API密钥等任何敏感信息
背景:
.net8 项目,需要连接 mysql 数据库,
开发环境:把连接字符串配置在 appsettings.Development.json 中
"ConnectionStrings": {
"RailCDE": "Server=localhost;port=3306;Database=RailCDE;Uid=root;Pwd=123456;SslMode=None;",
},
部署环境:
使用 consul kv 存储,参考:https://www.cnblogs.com/1285026182YUAN/p/19189386
连接字符串不配置密码,使用{password}占位符,在内存中替换,密码配置在 Docker Secrets 里面,用程序读取
"ConnectionStrings": {
"RailCDE": "Server=localhost;port=3306;Database=RailCDE;Uid=root;Pwd={password};SslMode=None;",
},
一. 生成密钥文件
1. 创建加密解密程序
public static class AesEncryption
{
/// <summary>
/// AES 加密(ECB 模式)
/// </summary>
/// <param name="plainText">明文</param>
/// <param name="aesKey">加密密钥(16, 24, 32 字节)</param>
/// <returns>Base64 编码的密文</returns>
public static string AesEcbEncrypt(string plainText, string aesKey)
{
if (string.IsNullOrEmpty(plainText) || string.IsNullOrEmpty(aesKey))
throw new ArgumentNullException("输入文本或密钥为空");
// 确保密钥长度符合要求
if (aesKey.Length != 16 && aesKey.Length != 24 && aesKey.Length != 32)
throw new ArgumentException("密钥长度无效,必须是 16、24 或 32 字节");
using (Aes aes = Aes.Create())
{
aes.Mode = CipherMode.ECB;
aes.Padding = PaddingMode.PKCS7;
aes.Key = Encoding.UTF8.GetBytes(aesKey);
// 创建加密器
ICryptoTransform encryptor = aes.CreateEncryptor();
byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);
byte[] encryptedBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
return Convert.ToBase64String(encryptedBytes);
}
}
/// <summary>
/// AES 解密(ECB 模式)
/// </summary>
/// <param name="cipherText">Base64 编码的密文</param>
/// <param name="aesKey">解密密钥(16, 24, 32 字节)</param>
/// <returns>解密后的明文</returns>
public static string AesEcbDecrypt(string cipherText, string aesKey)
{
if (string.IsNullOrEmpty(cipherText) || string.IsNullOrEmpty(aesKey))
throw new ArgumentNullException("输入密文或密钥为空");
// 确保密钥长度符合要求
if (aesKey.Length != 16 && aesKey.Length != 24 && aesKey.Length != 32)
throw new ArgumentException("密钥长度无效,必须是 16、24 或 32 字节");
using (Aes aes = Aes.Create())
{
aes.Mode = CipherMode.ECB;
aes.Padding = PaddingMode.PKCS7;
aes.Key = Encoding.UTF8.GetBytes(aesKey);
// 创建解密器
ICryptoTransform decryptor = aes.CreateDecryptor();
byte[] cipherBytes = Convert.FromBase64String(cipherText);
byte[] decryptedBytes = decryptor.TransformFinalBlock(cipherBytes, 0, cipherBytes.Length);
return Encoding.UTF8.GetString(decryptedBytes);
}
}
/// <summary>
/// 生成指定长度的随机密钥
/// </summary>
/// <param name="lengthInBytes">密钥的字节长度</param>
/// <returns>生成的密钥(Base64 编码)</returns>
public static string GenerateRandomKey(int lengthInBytes = 16)
{
using (var rng = RandomNumberGenerator.Create())
{
byte[] key = new byte[lengthInBytes];
rng.GetBytes(key); // 填充密钥数组
return Convert.ToBase64String(key); // 转换为 Base64 字符串
}
}
}
使用程序对密码加密
var aeskey = AesEncryption.GenerateRandomKey();//记录下来,解密时用
var pwdEn = AesEncryption.AesEcbEncrypt("密码", aeskey);
//var aeskey = Const.DBAESKEY;
var pwdDe= AesEncryption.AesEcbDecrypt(pwdEn, aeskey);
创建文件 D:\DockerConfig\configs\DbPwdMysql.txt
把生成的 pwdEn写入
二. 配置 docker-compose.yml 文件
version: '3.8'
services:
your-service:
image: your-image
environment:
- DB_PASSWORD_FILE=/run/secrets/db-pwd-mysql
secrets:
- db-pwd-mysql
secrets:
db-pwd-mysql:
file: D:\DockerConfig\configs\DbPwdMysql.txt #相对于 docker-compose.yml
三. 项目中解析
创建 UnifiedConfigurationBuilder.cs 文件
using Consul;
using Microsoft.Extensions.Configuration;
using Rail.Medium.Static;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace Rail.Medium.Middleware
{
public static class UnifiedConfigurationBuilder
{
public static IConfigurationBuilder AddUnifiedConfiguration(
this IConfigurationBuilder builder,
string consulAddress,
string appName)
{
// 1. 加载 Docker Secrets
builder.AddDockerSecrets();
// 2. 构建临时 IConfiguration
var tempConfig = builder.Build();
// 3. 加载 Consul 配置,并传入临时 IConfiguration
builder.Add(new ConsulConfigurationSource(consulAddress, appName, tempConfig));
// 4. 加载环境变量
builder.AddEnvironmentVariables();
return builder;
}
private static IConfigurationBuilder AddDockerSecrets(this IConfigurationBuilder builder)
{
//var secretsPath = @"D:\DockerConfig\configs";
var secretsPath = "/run/secrets";
if (Directory.Exists(secretsPath))
{
foreach (var file in Directory.GetFiles(secretsPath))
{
var key = Path.GetFileNameWithoutExtension(file);
var value_en = File.ReadAllText(file).Trim();
var value = AesEncryption.AesEcbDecrypt(value_en, Const.DBAESKEY);
builder.AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>($"Secrets:{key}", value)
});
}
}
return builder;
}
}
public class ConsulConfigurationSource : IConfigurationSource
{
private readonly string _consulAddress;
private readonly string _appName;
private readonly IConfiguration _configuration; // 新增
public ConsulConfigurationSource(string consulAddress, string appName, IConfiguration configuration)
{
_consulAddress = consulAddress;
_appName = appName;
_configuration = configuration;
}
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
return new ConsulConfigurationProvider(_consulAddress, _appName, _configuration, "config");
}
}
public class ConsulConfigurationProvider : ConfigurationProvider
{
private readonly string _consulAddress;
private readonly string _appName;
private readonly string _keyPrefix;
private readonly IConfiguration _configuration; // 新增
public ConsulConfigurationProvider(string consulAddress, string appName, IConfiguration configuration, string keyPrefix = "")
{
_consulAddress = consulAddress;
_appName = appName;
_keyPrefix = keyPrefix;
_configuration = configuration;
}
public override void Load()
{
LoadConfigs().GetAwaiter().GetResult();
}
private async Task LoadConfigs()
{
try
{
using var client = new ConsulClient(config =>
{
config.Address = new Uri(_consulAddress);
});
await LoadAllConfigKeys(client);
Console.WriteLine($"配置加载完成,共 {Data.Count} 个配置项");
}
catch (Exception ex)
{
Console.WriteLine($"加载配置失败: {ex.Message}");
}
}
private async Task LoadAllConfigKeys(ConsulClient client)
{
// 获取根目录下的所有键(根据你的前缀过滤)
var queryResult = await client.KV.List(_keyPrefix);
if (queryResult?.Response != null)
{
var newData = new Dictionary<string, string?>();
foreach (var kvPair in queryResult.Response)
{
if (kvPair.Value != null)
{
var key = RemovePrefix(kvPair.Key);
var value = Encoding.UTF8.GetString(kvPair.Value);
value = GetConnectionString(key, value, _configuration);
newData[key] = value;
}
}
Data = newData;
}
}
private string GetConnectionString(string key, string value, IConfiguration configuration)
{
// 判断是否为数据库连接,如果是,拼接密码
if (key.StartsWith("ConnectionStrings", StringComparison.OrdinalIgnoreCase))
{
// 从已经加载的 Secrets 配置中获取密码
var dbPwd = configuration["Secrets:DbPwdMysql"]; // 或根据 key 动态决定
if (!string.IsNullOrEmpty(dbPwd))
{
value = value.Replace("{password}", dbPwd); // 假设连接字符串里有 {password} 占位符
}
}
return value;
}
private string RemovePrefix(string key)
{
key = key.StartsWith(_keyPrefix) ? key.Substring(_keyPrefix.Length + 1) : key;
key = key.Replace("/", ":");
return key;
}
#region 手动重新加载接口
public async Task ReloadAsync()
{
await LoadConfigs();
OnReload(); // 通知配置已更新
Console.WriteLine("配置手动重新加载完成");
}
public Dictionary<string, string> GetCurrentConfigs()
{
return new Dictionary<string, string>(Data);
}
#endregion
}
}
Program.cs 文件
#region == consul kv 加载 ==
var consulKVPath = configuration.GetSection("ConsulKVPath");
if (consulKVPath.Exists())
{
builder.Configuration.AddUnifiedConfiguration(
consulAddress: consulKVPath.Value,
appName: builder.Environment.ApplicationName
);
}
#endregion
end.

浙公网安备 33010602011771号