【问题标题】:Storing production secrets in ASP.NET Core在 ASP.NET Core 中存储生产机密
【发布时间】:2017-03-01 02:38:55
【问题描述】:

我试图找出最好的位置来存储 ASP.NET Core 应用程序的应用程序生产机密。有两个类似的问题 Where should I store the connection string for the production environment of my ASP.NET Core app?How to deploy ASP.NET Core UserSecrets to production 两者都建议使用环境变量。

我的问题是我想使用不同的数据库和不同的数据库凭据运行我的 Web 应用程序的多个实例。所以应该有一些每个实例的配置,包括秘密。

如何以安全的方式实现这一点?

请注意,应用程序应该能够自托管并且可以在 IIS 下托管! (稍后我们还计划在 Linux 上运行它,如果这对问题有任何重要性)

更新

这个问题不是关于尝试在生产中使用 ASP.NET 用户机密! UserSecrets 被排除在生产环境之外。

【问题讨论】:

  • 关于用户机密:您至少不能将 UserSecrets 用于生产,因为 Secret Manager 工具不会对存储的机密进行加密,因此不应将其视为受信任的存储。它仅用于开发目的。键和值存储在用户配置文件目录中的 JSON 配置文件中。
  • 我不想使用 UserSecrets,这就是我问题的重点!我将编辑我的问题...

标签: asp.net-core


【解决方案1】:

如果您的应用程序托管在 AWS 上,此解决方案可能是最简单的。它依赖于SecretConfiguration.AwsKms NuGet 包。

秘密以加密形式存储在单独的配置文件中。然后在运行时使用AWS Key Management Service 解密这些秘密。通过这种方式,您可以将机密信息与应用程序的源代码一起进行版本控制,同时避免以明文形式存储机密信息。

具体来说,它是一个与Microsoft.Extensions.Configuration 堆栈集成的配置提供程序。

步骤如下:

1.创建 KMS 密钥

使用 AWS 控制台或 CLI 创建 KMS 密钥(对称加密)。确保您的开发人员拥有加密但不解密的权限,并且只有用于运行您的应用程序的角色才有解密权限。

2。加密您的秘密

使用 AWS CLI 加密您的密钥。要使用的命令如下(将key-id 替换为您的 KMS 密钥的密钥 ID):

aws kms encrypt --cli-binary-format raw-in-base64-out --key-id "11111111-0000-0000-0000-000000000000" --plaintext "SECRET_TO_ENCRYPT"

输出将包含密文:

{
    "CiphertextBlob": "AQICAHhDR/VQh6Ap...rfyKsKCG2h6WVK8=",
    "KeyId": "arn:aws:kms:eu-west-1:123456789:key/11111111-0000-0000-0000-000000000000",
    "EncryptionAlgorithm": "SYMMETRIC_DEFAULT"
}

对应用程序中的所有秘密重复此步骤。

3.为机密创建单独的配置文件

除了字符串值是加密的之外,此文件看起来就像您在 ASP.NET Core 应用程序中拥有的普通配置文件。例如,它可能看起来像:

{
  "Database": {
    "Password": "AQICAHhDR/VQh6Ap...rfyKsKCG2h6WVK8="
  },
  "Redis": {
    "Password": "AQICAHhDR/VQh6Ap...47iiHg/XifWcxvQ="
  }
}

如果您愿意,也可以为每个环境创建一个文件(例如 secrets.Staging.jsonsecrets.Production.json)。

4.启动时注册配置

在您的应用程序启动中,配置源配置,添加这个新的配置源:

string keyId = "arn:aws:kms:eu-west-1:123456789:key/11111111-0000-0000-0000-000000000000";

builder.Configuration.AddAwsKmsEncryptedConfiguration(
    new AmazonKeyManagementServiceClient(),
    keyId,
    encryptedSource => encryptedSource
        .SetBasePath(builder.Environment.ContentRootPath)
        .AddJsonFile($"secrets.{builder.Environment.EnvironmentName}.json"));

这将具有在运行时透明地解密秘密的效果,并将键/值配置对与已配置的其余配置源合并。

5.以与通常访问配置设置相同的方式访问您的机密

现在您可以像使用 IConfiguration 一样使用配置:

IConfiguration configuration;

// Comes from your regular configuration file
string databaseLogin = configuration["Database:Login"];
// Comes from the encrypted configuration file
string databasePassword = configuration["Database:Password"];

【讨论】:

    【解决方案2】:

    除了使用 Azure 应用服务或 docker 容器之外,您还可以使用 IDataProtector 在生产环境中安全地存储您的应用机密:

    1. 通过运行应用程序的 -config 开关输入应​​用程序机密,例如:dotnet helloworld -config;在Program.Main 中,检测到此开关以让用户输入机密并存储在单独的 .json 文件中,加密:
    public class Program
    {
        private const string APP_NAME = "5E71EE95-49BD-40A9-81CD-B1DFD873EEA8";
        private const string SECRET_CONFIG_FILE_NAME = "appsettings.secret.json";
    
        public static void Main(string[] args)
        {
            if (args != null && args.Length == 1 && args[0].ToLowerInvariant() == "-config")
            {
                ConfigAppSettingsSecret();
                return;
            }
            CreateWebHostBuilder(args).Build().Run();
        }
    
        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .ConfigureAppConfiguration((builder, options) =>
                {
                    options.AddJsonFile(ConfigFileFullPath, optional: true, reloadOnChange: false);
                })
                .UseStartup<Startup>();
    
        internal static IDataProtector GetDataProtector()
        {
            var serviceCollection = new ServiceCollection();
                
            serviceCollection.AddDataProtection()
                .SetApplicationName(APP_ID)
                .PersistKeysToFileSystem(new DirectoryInfo(SecretsDirectory));
            var services = serviceCollection.BuildServiceProvider();
            var dataProtectionProvider = services.GetService<IDataProtectionProvider>();
            return dataProtectionProvider.CreateProtector(APP_ID);
        }
    
        private static void ConfigAppSettingsSecret()
        {
            var protector = GetDataProtector();
    
            string dbPassword = protector.Protect("DbPassword", ReadPasswordFromConsole());
            ... // other secrets
            string json = ...;  // Serialize encrypted secrets to JSON
            var path = ConfigFileFullPath;
            File.WriteAllText(path, json);
            Console.WriteLine($"Writing app settings secret to '${path}' completed successfully.");
        }
    
        private static string CurrentDirectory
        {
            get { return Directory.GetParent(typeof(Program).Assembly.Location).FullName; }
        }
    
        private static string ConfigFileFullPath
        {
            get { return Path.Combine(CurrentDirectory, SECRET_CONFIG_FILE_NAME); }
        }
    }
    
    1. 在 Startup.cs 中,读取并解密密钥:
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        ...
        if (env.IsProduction())
        {
            var protector = Program.GetDataProtector();
            var builder = new SqlConnectionStringBuilder();
            builder.Password = protector.Unprotect(configuration["DbPassword"]);
            ...
        }
    }
    

    顺便说一句,appsettings.production.json 或环境变量真的不是一个选项。顾名思义,秘密永远不应该以纯文本形式存储。

    【讨论】:

    • 这是否意味着每次应用启动时都需要手动输入密码?如果托管的 IIS 在半夜自动重启会发生什么情况,应用程序将在用户干预之前运行。
    • 当然不是,你只需要在-config switch 下运行你的程序一次。
    • 这个解决方案如何工作?已在 ConfigureServices 方法中使用连接字符串打开数据库,并调用 services.AddDbContext。当您使用 Configure 方法时,此时解密 db 密码并打开数据库为时已晚。还是我在这里遗漏了什么?
    • @BryanCass 我明白你的意思。我已经用新的Program.GetDataProtector 方法更新了答案。诀窍是ServiceCollection.BuildServiceProvider()。更新后的答案使用单独的ServiceCollection 来获取IDataProtectionProvider,这样更简洁。重点是:你可以在任何地方获得IDataProtectionProvder
    • 您的代码中仍有“protector.Protect("DbPassword", ReadPasswordFromConsole())”。你又回到了原点。黑客可以从您的程序二进制文件中提取密码。
    【解决方案3】:

    正如他们所说,用户机密用于开发(以避免将凭据意外提交到 SCM),而不是用于生产。您应该为每个数据库使用一个连接字符串,即ConnectionStrings:CmsDatabaseProductionConnectionStrings:CmsDatabaseDevelopment 等。

    或者使用 docker 容器(当你不使用 Azure 应用服务时),那么你可以在每个容器的基础上进行设置。

    或者,您也可以使用基于环境的应用设置文件。 appsettings.production.json,但不能将它们包含在源代码控制管理(Git、CSV、TFS)中!

    在启动时只做:

        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
                .AddEnvironmentVariables();
            Configuration = builder.Build();
        }
    

    这样,您可以从appsettings.production.json 加载特定内容,并且仍然可以通过环境变量覆盖它。

    【讨论】:

    • 我知道在开发期间用户机密的问题。但这不是这里的问题,我只是在谈论生产时间!环境设置文件没有帮助,因为它对于生产中的每个实例仍然是相同的,并且所有实例的覆盖环境变量仍然是相同的!我现在对 Docker 的了解还不够,但也许这个想法会有所帮助。
    • @NicolasR:不一定。每次安装都有一个 application.production.json。如果您的应用程序位于 3 个不同的文件夹中,则可以从源代码管理中设置它们的每个 applciation.production.json´ to the desired value. You just have to make sure to exclude applciation.production.json´,因为它现在包含敏感数据。
    • @NicolasR:嗯,据我所知,没有像传统 ASP.NET 应用程序那样的“安全字符串”,您可以将加密字符串放入 web.config 并拥有它在运行时解密,因此除非您编写自己的解密方法(它将读取加密字符串并通过私钥/公钥解密),否则无法存储它。但这影响太大了。您的应用需要访问私钥来解密字符串并且需要此私钥的密码,因此您仍然必须让应用访问它
    • 如果有人入侵了您的服务器,他还可以访问您的私钥和密码以及您的应用程序(如果密码是硬编码的)。编译为 CIL 的 C# 代码根本不提供安全性。因此,获得访问您服务器的权限的人将有办法以一种或另一种方式访问​​您的密码。即使他们能够获得密码,您的数据库也不应该面向互联网,并且只允许来自您的网络/IP/本地的连接。安全性这比加密连接字符串提供了更多的安全性(因为你必须解密它们)
    • @Tseng 如果您不将 appsettings.{whatever}.json 添加到您的存储库中,您如何期望任何 CI/CD 管道在部署应用程序时了解每个环境的特定设置。那根本行不通。 appsettings 文件不是存放秘密的地方,而是存放设置的地方。必须将它们添加到 Git(或您使用的任何其他东西)中才能被您的应用程序使用。
    猜你喜欢
    • 1970-01-01
    • 2018-11-03
    • 2020-07-15
    • 2022-09-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-10-27
    • 1970-01-01
    相关资源
    最近更新 更多