From c174a6bfb4db54eee646099b11f2d68cab6d1fc3 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Fri, 8 Aug 2025 13:09:45 +0200 Subject: [PATCH] Update DataProtectionExtensions to load secrets from file when running under docker (#1098) --- .../DataProtectionExtensions.cs | 103 ++++++++++++------ 1 file changed, 68 insertions(+), 35 deletions(-) diff --git a/apps/server/Utilities/Cryptography/AliasVault.Cryptography.Server/DataProtectionExtensions.cs b/apps/server/Utilities/Cryptography/AliasVault.Cryptography.Server/DataProtectionExtensions.cs index 62b2f3d28..ed43a96d0 100644 --- a/apps/server/Utilities/Cryptography/AliasVault.Cryptography.Server/DataProtectionExtensions.cs +++ b/apps/server/Utilities/Cryptography/AliasVault.Cryptography.Server/DataProtectionExtensions.cs @@ -28,17 +28,8 @@ public static class DataProtectionExtensions this IServiceCollection services, string applicationName) { - var certPassword = Environment.GetEnvironmentVariable("DATA_PROTECTION_CERT_PASS") - ?? throw new KeyNotFoundException("DATA_PROTECTION_CERT_PASS is not set in configuration or environment variables."); - var certPath = $"../../certificates/app/{applicationName}.DataProtection.pfx"; - if (certPassword == "Development") - { - certPath = Path.Combine(AppContext.BaseDirectory, $"{applicationName}.DataProtection.Development.pfx"); - } - - // Use different protection methods for containerized environments - var isContainer = Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == "true" || - Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Production"; + // Determine if running in a container + var isContainer = Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == "true"; var dataProtectionBuilder = services.AddDataProtection() .PersistKeysToDbContext() @@ -46,35 +37,77 @@ public static class DataProtectionExtensions if (isContainer) { - // When running in containers, don't use certificate-based key protection due to Linux keystore limitations - // Keys are protected by database access controls, TLS, and container isolation - dataProtectionBuilder - .UseCryptographicAlgorithms(new Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.AuthenticatedEncryptorConfiguration() - { - EncryptionAlgorithm = Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.EncryptionAlgorithm.AES_256_CBC, - ValidationAlgorithm = Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ValidationAlgorithm.HMACSHA256, - }) - .SetDefaultKeyLifetime(TimeSpan.FromDays(90)); + ConfigureContainerDataProtection(dataProtectionBuilder); } else { - // Use certificate-based protection for development - var certificateFlags = X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable; - - X509Certificate2 cert; - if (!File.Exists(certPath)) - { - cert = CertificateGenerator.GeneratePfx($"{applicationName}.DataProtection", certPassword); - CertificateGenerator.SaveCertificateToFile(cert, certPassword, certPath); - } - else - { - cert = X509CertificateLoader.LoadPkcs12FromFile(certPath, certPassword, certificateFlags); - } - - dataProtectionBuilder.ProtectKeysWithCertificate(cert); + ConfigureDevelopmentDataProtection(dataProtectionBuilder, applicationName); } return services; } + + /// + /// Configure data protection for container environments. + /// + /// The data protection builder. + private static void ConfigureContainerDataProtection(IDataProtectionBuilder dataProtectionBuilder) + { + // In container, load password from file + var certPassPath = "/secrets/data_protection_cert_pass"; + if (!File.Exists(certPassPath)) + { + throw new KeyNotFoundException($"Certificate password file not found at {certPassPath}."); + } + + var certPassword = File.ReadAllText(certPassPath).Trim(); + if (string.IsNullOrEmpty(certPassword)) + { + throw new KeyNotFoundException($"Certificate password file at {certPassPath} is empty."); + } + + // When running in containers, don't use certificate-based key protection due to Linux keystore limitations + // Keys are protected by database access controls, TLS, and container isolation + dataProtectionBuilder + .UseCryptographicAlgorithms(new Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.AuthenticatedEncryptorConfiguration() + { + EncryptionAlgorithm = Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.EncryptionAlgorithm.AES_256_CBC, + ValidationAlgorithm = Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ValidationAlgorithm.HMACSHA256, + }) + .SetDefaultKeyLifetime(TimeSpan.FromDays(90)); + } + + /// + /// Configure data protection for development environments. + /// + /// The data protection builder. + /// The application name. + private static void ConfigureDevelopmentDataProtection(IDataProtectionBuilder dataProtectionBuilder, string applicationName) + { + // Not in container, require environment variable + var certPassword = Environment.GetEnvironmentVariable("DATA_PROTECTION_CERT_PASS") + ?? throw new KeyNotFoundException("DATA_PROTECTION_CERT_PASS is not set in configuration or environment variables."); + + var certPath = $"../../certificates/app/{applicationName}.DataProtection.pfx"; + if (certPassword == "Development") + { + certPath = Path.Combine(AppContext.BaseDirectory, $"{applicationName}.DataProtection.Development.pfx"); + } + + // Use certificate-based protection for development + var certificateFlags = X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable; + + X509Certificate2 cert; + if (!File.Exists(certPath)) + { + cert = CertificateGenerator.GeneratePfx($"{applicationName}.DataProtection", certPassword); + CertificateGenerator.SaveCertificateToFile(cert, certPassword, certPath); + } + else + { + cert = X509CertificateLoader.LoadPkcs12FromFile(certPath, certPassword, certificateFlags); + } + + dataProtectionBuilder.ProtectKeysWithCertificate(cert); + } }