在带有 IdentityServer4 certification/http 错误的 Azure Web 应用程序容器上部署 .net Core 3 linux 容器

Deploying .net Core 3 linux container on azure web app container with IdentityServer4 certification/http error

我正在尝试使用 .Net Core Clean Architecture App Template 并在容器中获取它 运行 并通过 azure CI/CD 管道

进行部署

我在 linux 容器中本地安装了模板 运行 的容器化版本,端口为 5001,一切正常。

我的 azure 管道构建过程正常工作,它在我的容器注册表中创建映像。

问题 一旦我 deploy/release 到容器的 Web 应用程序,应用程序失败并抛出以下错误:

Application startup exception System.InvalidOperationException: Couldn't find a valid certificate with subject 'CN=localhost' on the 'CurrentUser\My' at Microsoft.AspNetCore.ApiAuthorization.IdentityServer.SigningKeysLoader.LoadFromStoreCert(String subject, String storeName, StoreLocation storeLocation, DateTimeOffset currentTime)

我做了什么:

  1. 以下 these docs from MS 我创建了一个本地开发证书:

    dotnet dev-certs https -ep %USERPROFILE%\.aspnet\https\aspnetapp.pfx -p { password here }

    dotnet dev-certs https --trust

  2. 然后我将其作为私有 .pfx 证书导入 Web 应用程序。

  3. 我添加了一个应用程序设置 WEBSITE_LOAD_CERTIFICATES 带有证书的“thumb”值

  4. 我在身份服务器appSettings.json部分使用了导入证书的“主机名”(在我的例子中是主机名=localhost)

加载 Web 应用程序时,它显示 :( 应用程序错误,docker 日志给出了我上面引用的错误。

我很确定这与身份服务器设置和此处的 appSettings.json 值有关:

  "IdentityServer": {
    "Key": {
      "Type": "Store",
      "StoreName": "My",
      "StoreLocation": "CurrentUser",
      "Name": "CN=localhost"
    }
  }

谁能帮我解决这个错误?

编辑 1 - 为 IdentityServer 密钥手动指定文件

这肯定与身份服务器有关。我尝试手动将证书设置为 appSettings.json 中的文件,如下所示:

  "IdentityServer": {
    "Key": {
      "Type": "File",
      "FilePath": "aspnetapp.pfx",
      "Password": "Your_password123"
    }
  }

现在我得到这个错误:

Loading certificate file at '/app/aspnetapp.pfx' with storage flags ''. Application startup exception System.InvalidOperationException: There was an error loading the certificate. The file '/app/aspnetapp.pfx' was not found. Microsoft.AspNetCore.ApiAuthorization.IdentityServer.SigningKeysLoader.LoadFromFile

我将其添加到 docker 文件中:

WORKDIR /app
COPY ["/aspnetapp.pfx", "/app"]
RUN find /app

正如您从下图中看到的,文件显示在应用程序的构建目录中:

我还确保 aspnetapp.pfx 不会被 .gitignore 或 .dockerignore 文件忽略。

我不明白为什么它不加载这个文件。它似乎存在于它应该存在的地方。

使用证书缩略图和更新的路径编辑 2

所以我使用了 tnc1977 建议并将其作为身份密钥的设置

  "IdentityServer": {
    "Key": {
      "Type": "File",
      "FilePath": "/var/ssl/private/<thumb_value>.p12",
      "Password": "Your_password123"
    }
  }

然而,这给出了另一个错误:

There was an error loading the certificate. Either the password is incorrect or the process does not have permisions to store the key in the Keyset 'EphemeralKeySet' Interop+Crypto+OpenSslCryptographicException: error:23076071:PKCS12 routines:PKCS12_parse:mac verify failure

编辑 3:有效的 Azure App 证书

我购买了一个 Azure App Certificate 并添加了一个带有 TSL 设置的自定义域,但出现了同样的错误

编辑 4:在代码中加载证书 startup.cs - 新错误:

我现在知道我不能使用证书库 CurrentUser/My,因为那是 windows 的。 Linux 容器必须在代码中手动加载证书。

我正在使用已添加到 Azure Web 应用程序的应用程序证书的指纹。它是一个私有的 Azure 应用程序证书,并且已经针对自定义域进行了验证。

我将此代码添加到我的 statup.cs 配置服务中(我知道对这些值进行硬编码不是最佳做法,但我只想看看它是否可以加载证书,我将切换到 env 变量和密钥库) :

        // linux file path for private keys
        var cryptBytes = File.ReadAllBytes("/var/ssl/private/<thumbprint>.p12");
        var cert = new X509Certificate2(cryptBytes, "");

        services.AddIdentityServer().AddSigningCredential(cert);

我输入了一个空白密码,因为我认为这是你应该做的。我现在在我的 docker 日志中收到以下错误,这让我相信证书已加载,现在该错误与我同时使用 services.AddIdentityServer() .AddSigningCredential(cert); 在 startup.cs configureservicesapp.UseIdentityServer( ) 在startup.cs 配置:

Unhandled exception. System.InvalidOperationException: Decorator already registered for type: IAuthenticationService.

我不确定如何将证书添加到 app.UseIdentityServer();行。

编辑 5

经过更多的挖掘,不幸的是@tnc1997 的回答是行不通的。 在 asp.net 核心 3 调用 app.UseIdentityServer 在我的 satrtup.cs 在内部崇敬一种在应用程序设置(环境)中查找身份服务器密钥、文件、密码等的方法。json 文件。

因此,即使我在 tnc1997 显示的代码中加载证书,应用程序仍会在设置文件中查找。因此设置文件必须包含 IS4 密钥的正确详细信息。

此外,azure 不会将证书放置在 linux 容器中典型的受信任位置。据我所知,执行此操作的唯一方法似乎是安装一个卷(在本例中为 azure 存储文件共享)并使用上传到该文件共享的证书。

我可以确认这在本地有效,但现在我仍然遇到问题 运行 容器,前端加载并且 Web api 项目似乎没有启动。我将 post 另一个问题来解决这个问题。

我认为问题在于您在容器中的应用程序不信任本地创建的开发人员证书。它只能在您的机器上使用,因为您的计算机上安装了开发根证书。

容器永远不会信任 dotnet dev-certs 创建的证书。

您需要获得适当信任的证书,例如来自 LetsEncrypt

原答案

我认为问题可能是您试图使用 Windows 证书存储在 Linux 容器中加载证书。

文档 here 很好地概述了如何在 Linux 托管应用中使用应用服务私有证书:

  1. 在 Azure 门户中,从左侧菜单中,select 应用服务 >
  2. 从您应用的左侧导航中,select TLS/SSL 设置,然后 select 私钥证书 (.pfx) 或 Public 密钥证书 (.cer) .
  3. 找到您要使用的证书并复制指纹。
  4. 要在您的应用程序代码中访问证书,请将其指纹添加到 WEBSITE_LOAD_CERTIFICATES 应用程序设置中。
  5. WEBSITE_LOAD_CERTIFICATES 应用程序设置使指定的证书可供您的 Linux 托管应用程序(包括自定义容器应用程序)作为文件访问。这些文件位于以下目录下:
    • 私人证书 - /var/ssl/private(.p12 文件)
    • Public 证书 - /var/ssl/certs(.der 文件)
  6. 使用下面的代码示例将指定的证书加载到您的 Linux 托管应用程序(包括自定义容器应用程序)中:
    using System;
    using System.IO;
    using System.Security.Cryptography.X509Certificates;
    
    var bytes = File.ReadAllBytes($"/var/ssl/private/{Configuration["WEBSITE_LOAD_CERTIFICATES"]}.p12");
    var cert = new X509Certificate2(bytes);
    

签署凭据

以下是我用来生成签名凭据的步骤:

  1. 安装OpenSSL.
  2. 生成私钥和public证书。
    1. 运行 openssl req -x509 -newkey rsa:4096 -sha256 -nodes -keyout example.com.key -out example.com.crt -subj "/CN=example.com" -days 365example.com 替换为站点名称。
  3. 将以上内容合并为一个 PFX 文件。
    1. 运行 openssl pkcs12 -export -out example.com.pfx -inkey example.com.key -in example.com.crtexample.com 替换为站点名称。
  4. 将 PFX 文件上传到 Azure。
    1. 在 Azure 门户中,从左侧菜单中,select 应用服务 >
    2. 从您的应用程序的左侧导航中,select TLS/SSL 设置,然后 select 私钥证书 (.pfx),然后上传上述 PFX 文件。
  5. 配置应用设置。
    1. 将上述 PFX 文件的指纹添加到应用服务中的 WEBSITE_LOAD_CERTIFICATES 应用设置。

IdentityServer

下面的代码示例显示了一个完整的 Startup.cs 配置,可用于启动 IdentityServer 应用程序和 运行:

namespace IdentityServer
{
    public class Startup
    {
        public Startup(IConfiguration configuration, IWebHostEnvironment environment)
        {
            Configuration = configuration;
            Environment = environment;
        }

        public IConfiguration Configuration { get; }

        public IWebHostEnvironment Environment { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            void ConfigureDbContext(DbContextOptionsBuilder builder)
            {
                builder.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"));
            }

            var builder = services.AddIdentityServer()
                .AddConfigurationStore(options => { options.ConfigureDbContext = ConfigureDbContext; })
                .AddOperationalStore(options => { options.ConfigureDbContext = ConfigureDbContext; });

            if (Environment.IsDevelopment())
            {
                builder.AddDeveloperSigningCredential();
            }
            else
            {
                try
                {
                    var bytes = File.ReadAllBytes($"/var/ssl/private/{Configuration["WEBSITE_LOAD_CERTIFICATES"]}.p12");
                    var certificate = new X509Certificate2(bytes);
                    builder.AddSigningCredential(certificate);
                }
                catch (FileNotFoundException)
                {
                    throw new Exception($"The certificate with the thumbprint \"{Configuration["WEBSITE_LOAD_CERTIFICATES"].Substring(0, 8)}...\" could not be found.");
                }
            }
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment()) app.UseDeveloperExceptionPage();

            app.UseIdentityServer();
        }
    }
}

简洁的架构

下面的代码示例显示了一个完整的 DependencyInjection.cs 配置,可用于启动 Clean Architecture 应用程序和 运行:

namespace CleanArchitecture.Infrastructure
{
    public static class DependencyInjection
    {
        public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
        {
            void ConfigureDbContext(DbContextOptionsBuilder builder)
            {
                if (configuration.GetValue<bool>("UseInMemoryDatabase"))
                {
                    builder.UseInMemoryDatabase("CleanArchitectureDb");
                }
                else
                {
                    builder.UseSqlServer(configuration.GetConnectionString("DefaultConnection"), b => b.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName));
                }
            }

            services.AddDbContext<ApplicationDbContext>(ConfigureDbContext);

            services.AddScoped<IApplicationDbContext>(provider => provider.GetService<ApplicationDbContext>());

            services.AddScoped<IDomainEventService, DomainEventService>();

            services.AddDefaultIdentity<ApplicationUser>()
                .AddEntityFrameworkStores<ApplicationDbContext>();

            var builder = services.AddIdentityServer()
                .AddConfigurationStore(options => { options.ConfigureDbContext = ConfigureDbContext; })
                .AddOperationalStore(options => { options.ConfigureDbContext = ConfigureDbContext; })
                .AddAspNetIdentity<ApplicationUser>();

            var bytes = File.ReadAllBytes($"/var/ssl/private/{Configuration["WEBSITE_LOAD_CERTIFICATES"]}.p12");
            var certificate = new X509Certificate2(bytes);
            builder.AddSigningCredential(certificate);

            services.AddTransient<IDateTime, DateTimeService>();
            services.AddTransient<IIdentityService, IdentityService>();
            services.AddTransient<ICsvFileBuilder, CsvFileBuilder>();

            services.AddAuthentication()
                .AddIdentityServerJwt();

            return services;
        }
    }
}

.Net Clean Architecture is calling services.AddIdentityServer() .AddApiAuthorization<ApplicationUser, ApplicationDbContext>(); in DependencyInjection.cs which is extension method. This method internally calls bunch of other methods one of which is .AddSigningCredentials(). Unfortunately this default method will fail in Linux environment because it cannot read bare private keys. According to this issue你需要在Linux中自己构建PFX。

我认为的解决方案:

  1. 删除.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
  2. 写自己的方法
var bytes = File.ReadAllBytes($"/var/ssl/private/{thump_print_goes_here}.p12");
var certificate = new X509Certificate2(bytes);
var builder = services.AddIdentityServer()
                .AddAspNetIdentity<ApplicationUser>()
                .AddOperationalStore<ApplicationDbContext>()
                .AddIdentityResources()
                .AddApiResources()
                .AddClients()
                .AddSigningCredential(certificate);

我遇到了这个问题 运行 linux 应用服务上的 .net 核心 spa 模板。我还创建了一个自签名的 .pfx,如 tnc1997 的回答中所述。虽然答案可以拼凑起来,但对我来说问题是:

  • 引用您的证书路径时,请勿使用上传的 .pfx 文件名。相反,如前所述,您的证书文件获得一个新名称“.p12”并且(在 linux 容器中)在“/var/ssl/private/”下找到。
  • 指定一个空白密码。不要为上传的 .pfx 文件指定密码。而是将 appsetting“IdentityServer__Key__Password”设置为“”(空)。

编辑 2 中提供的解决方案有效。我已经在 Azure 上的 Linux 应用服务中使用我的 .NET6 应用测试并验证了这一点。

我的 appsettings.Production.json 看起来像这样:

{
    "IdentityServer": {
        "Key": {
            "Type": "File",
            "FilePath": "/var/ssl/private/{thumbprintGoesHere}.p12",
            "Password": ""
          }
    }
}

我已按照本指南 https://docs.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/hosted-with-identity-server?view=aspnetcore-5.0&tabs=visual-studio 在 Azure Key Vaults 中创建我的 SSL 证书和 SigningKey 证书作为自签名密钥。我使用 WEBSITE_LOAD_CERTIFICATES 将证书加载到 /var/ssl/private/ 路径。

我希望这能帮助遇到同样问题的人。