mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-24 08:17:57 -04:00
Add generic secretreader to support files when running in docker (#1098)
This commit is contained in:
committed by
Leendert de Borst
parent
c174a6bfb4
commit
8bd8d688ef
@@ -5,7 +5,6 @@
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using AliasServerDb;
|
||||
using AliasServerDb.Configuration;
|
||||
@@ -19,6 +18,7 @@ using AliasVault.Logging;
|
||||
using AliasVault.RazorComponents.Services;
|
||||
using AliasVault.Shared.Models.Configuration;
|
||||
using AliasVault.Shared.Server.Services;
|
||||
using AliasVault.Shared.Server.Utilities;
|
||||
using ApexCharts;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
@@ -30,13 +30,13 @@ builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnC
|
||||
builder.Configuration.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true);
|
||||
builder.Services.ConfigureLogging(builder.Configuration, Assembly.GetExecutingAssembly().GetName().Name!, "../../logs");
|
||||
|
||||
// Create global config object, get values from environment variables.
|
||||
// Create global config object, get values from environment variables or container secrets.
|
||||
var config = new Config();
|
||||
var adminPasswordHash = Environment.GetEnvironmentVariable("ADMIN_PASSWORD_HASH") ?? throw new KeyNotFoundException("ADMIN_PASSWORD_HASH environment variable is not set.");
|
||||
config.AdminPasswordHash = adminPasswordHash;
|
||||
|
||||
var lastPasswordChanged = Environment.GetEnvironmentVariable("ADMIN_PASSWORD_GENERATED") ?? throw new KeyNotFoundException("ADMIN_PASSWORD_GENERATED environment variable is not set.");
|
||||
config.LastPasswordChanged = DateTime.Parse(lastPasswordChanged, CultureInfo.InvariantCulture);
|
||||
// Get admin password hash and generation timestamp using SecretReader
|
||||
var (adminPasswordHash, lastPasswordChanged) = SecretReader.GetAdminPasswordHash();
|
||||
config.AdminPasswordHash = adminPasswordHash;
|
||||
config.LastPasswordChanged = lastPasswordChanged;
|
||||
|
||||
var ipLoggingEnabled = Environment.GetEnvironmentVariable("IP_LOGGING_ENABLED") ?? "false";
|
||||
config.IpLoggingEnabled = bool.Parse(ipLoggingEnabled);
|
||||
|
||||
@@ -22,6 +22,7 @@ using AliasVault.Shared.Models.WebApi.Auth;
|
||||
using AliasVault.Shared.Models.WebApi.PasswordChange;
|
||||
using AliasVault.Shared.Providers.Time;
|
||||
using AliasVault.Shared.Server.Services;
|
||||
using AliasVault.Shared.Server.Utilities;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
@@ -615,19 +616,13 @@ public class AuthController(IAliasServerDbContextFactory dbContextFactory, UserM
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the JWT key from the environment variables.
|
||||
/// Get the JWT key from the environment variables or container secrets.
|
||||
/// </summary>
|
||||
/// <returns>JWT key as string.</returns>
|
||||
/// <exception cref="KeyNotFoundException">Thrown if environment variable does not exist.</exception>
|
||||
/// <exception cref="KeyNotFoundException">Thrown if JWT key cannot be found.</exception>
|
||||
private static string GetJwtKey()
|
||||
{
|
||||
var jwtKey = Environment.GetEnvironmentVariable("JWT_KEY");
|
||||
if (jwtKey is null)
|
||||
{
|
||||
throw new KeyNotFoundException("JWT_KEY environment variable is not set.");
|
||||
}
|
||||
|
||||
return jwtKey;
|
||||
return SecretReader.GetJwtKey();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -20,6 +20,7 @@ using AliasVault.Logging;
|
||||
using AliasVault.Shared.Models.Configuration;
|
||||
using AliasVault.Shared.Providers.Time;
|
||||
using AliasVault.Shared.Server.Services;
|
||||
using AliasVault.Shared.Server.Utilities;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
@@ -94,11 +95,7 @@ builder.Services.AddAuthentication(options =>
|
||||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
}).AddJwtBearer(options =>
|
||||
{
|
||||
var jwtKey = Environment.GetEnvironmentVariable("JWT_KEY");
|
||||
if (jwtKey is null)
|
||||
{
|
||||
throw new KeyNotFoundException("JWT_KEY environment variable is not set.");
|
||||
}
|
||||
var jwtKey = SecretReader.GetJwtKey();
|
||||
|
||||
options.IncludeErrorDetails = true;
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="SecretReader.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Shared.Server.Utilities;
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
|
||||
/// <summary>
|
||||
/// Utility class for reading secrets from environment variables or container secret files.
|
||||
/// </summary>
|
||||
public static class SecretReader
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines if the application is running in a container.
|
||||
/// </summary>
|
||||
/// <returns>True if running in a container, false otherwise.</returns>
|
||||
public static bool IsRunningInContainer()
|
||||
{
|
||||
return Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == "true";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the JWT key from either the container secrets file or environment variable.
|
||||
/// </summary>
|
||||
/// <returns>The JWT key.</returns>
|
||||
/// <exception cref="KeyNotFoundException">Thrown when the JWT key cannot be found or is invalid.</exception>
|
||||
public static string GetJwtKey()
|
||||
{
|
||||
if (IsRunningInContainer())
|
||||
{
|
||||
return ReadSecretFromFile("/secrets/jwt_key", "JWT key");
|
||||
}
|
||||
|
||||
var jwtKey = Environment.GetEnvironmentVariable("JWT_KEY");
|
||||
if (string.IsNullOrEmpty(jwtKey))
|
||||
{
|
||||
throw new KeyNotFoundException("JWT_KEY environment variable is not set.");
|
||||
}
|
||||
|
||||
return jwtKey;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the admin password hash and generation timestamp.
|
||||
/// </summary>
|
||||
/// <returns>A tuple containing the password hash and the timestamp when it was generated.</returns>
|
||||
/// <exception cref="KeyNotFoundException">Thrown when the admin password hash cannot be found.</exception>
|
||||
/// <exception cref="InvalidOperationException">Thrown when the admin password hash file has an invalid format.</exception>
|
||||
public static (string PasswordHash, DateTime GeneratedAt) GetAdminPasswordHash()
|
||||
{
|
||||
if (IsRunningInContainer())
|
||||
{
|
||||
var secretsFilePath = "/secrets/admin_password_hash";
|
||||
if (!File.Exists(secretsFilePath))
|
||||
{
|
||||
throw new KeyNotFoundException($"Admin password hash file not found at {secretsFilePath}. Container initialization may have failed.");
|
||||
}
|
||||
|
||||
var secretContent = File.ReadAllText(secretsFilePath).Trim();
|
||||
if (string.IsNullOrEmpty(secretContent))
|
||||
{
|
||||
throw new KeyNotFoundException($"Admin password hash file at {secretsFilePath} is empty.");
|
||||
}
|
||||
|
||||
// Parse hash and timestamp separated by |
|
||||
var parts = secretContent.Split('|');
|
||||
if (parts.Length != 2)
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid format in {secretsFilePath}. Expected format: hash|timestamp");
|
||||
}
|
||||
|
||||
var passwordHash = parts[0];
|
||||
if (!DateTime.TryParse(parts[1], CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var generatedAt))
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid timestamp format in {secretsFilePath}: {parts[1]}");
|
||||
}
|
||||
|
||||
return (passwordHash, generatedAt);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not in container - use environment variables
|
||||
var passwordHash = Environment.GetEnvironmentVariable("ADMIN_PASSWORD_HASH")
|
||||
?? throw new KeyNotFoundException("ADMIN_PASSWORD_HASH environment variable is not set.");
|
||||
|
||||
var generatedAtStr = Environment.GetEnvironmentVariable("ADMIN_PASSWORD_GENERATED")
|
||||
?? throw new KeyNotFoundException("ADMIN_PASSWORD_GENERATED environment variable is not set.");
|
||||
|
||||
var generatedAt = DateTime.Parse(generatedAtStr, CultureInfo.InvariantCulture);
|
||||
return (passwordHash, generatedAt);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the data protection certificate password.
|
||||
/// </summary>
|
||||
/// <returns>The data protection certificate password.</returns>
|
||||
/// <exception cref="KeyNotFoundException">Thrown when the certificate password cannot be found.</exception>
|
||||
public static string GetDataProtectionCertPassword()
|
||||
{
|
||||
if (IsRunningInContainer())
|
||||
{
|
||||
return ReadSecretFromFile("/secrets/data_protection_cert_pass", "Certificate password");
|
||||
}
|
||||
|
||||
return Environment.GetEnvironmentVariable("DATA_PROTECTION_CERT_PASS")
|
||||
?? throw new KeyNotFoundException("DATA_PROTECTION_CERT_PASS is not set in configuration or environment variables.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a secret from a file in the container secrets directory.
|
||||
/// </summary>
|
||||
/// <param name="filePath">The path to the secret file.</param>
|
||||
/// <param name="secretName">The name of the secret for error messages.</param>
|
||||
/// <returns>The secret value.</returns>
|
||||
/// <exception cref="KeyNotFoundException">Thrown when the secret file cannot be found or is invalid.</exception>
|
||||
private static string ReadSecretFromFile(string filePath, string secretName)
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
throw new KeyNotFoundException($"{secretName} file not found at {filePath}. Container initialization may have failed.");
|
||||
}
|
||||
|
||||
var secretValue = File.ReadAllText(filePath).Trim();
|
||||
if (string.IsNullOrEmpty(secretValue))
|
||||
{
|
||||
throw new KeyNotFoundException($"{secretName} file at {filePath} is empty.");
|
||||
}
|
||||
|
||||
return secretValue;
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\Databases\AliasServerDb\AliasServerDb.csproj" />
|
||||
<ProjectReference Include="..\..\..\Shared\AliasVault.Shared.Server\AliasVault.Shared.Server.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace AliasVault.Cryptography.Server;
|
||||
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using AliasServerDb;
|
||||
using AliasVault.Shared.Server.Utilities;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
@@ -28,14 +29,11 @@ public static class DataProtectionExtensions
|
||||
this IServiceCollection services,
|
||||
string applicationName)
|
||||
{
|
||||
// Determine if running in a container
|
||||
var isContainer = Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == "true";
|
||||
|
||||
var dataProtectionBuilder = services.AddDataProtection()
|
||||
.PersistKeysToDbContext<AliasServerDbContext>()
|
||||
.SetApplicationName(applicationName);
|
||||
|
||||
if (isContainer)
|
||||
if (SecretReader.IsRunningInContainer())
|
||||
{
|
||||
ConfigureContainerDataProtection(dataProtectionBuilder);
|
||||
}
|
||||
@@ -53,19 +51,6 @@ public static class DataProtectionExtensions
|
||||
/// <param name="dataProtectionBuilder">The data protection builder.</param>
|
||||
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
|
||||
@@ -84,9 +69,8 @@ public static class DataProtectionExtensions
|
||||
/// <param name="applicationName">The application name.</param>
|
||||
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.");
|
||||
// Not in container, get certificate password using SecretReader
|
||||
var certPassword = SecretReader.GetDataProtectionCertPassword();
|
||||
|
||||
var certPath = $"../../certificates/app/{applicationName}.DataProtection.pfx";
|
||||
if (certPassword == "Development")
|
||||
|
||||
Reference in New Issue
Block a user