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.

 

posted @ 2025-11-05 09:55  无心々菜  阅读(22)  评论(0)    收藏  举报