Add reset admin password script for all-in-one image (#1108)

This commit is contained in:
Leendert de Borst
2025-08-11 20:23:15 +02:00
committed by Leendert de Borst
parent db874d3799
commit d587f3fd5c
10 changed files with 396 additions and 73 deletions

View File

@@ -95,6 +95,51 @@ jobs:
fi
echo "✅ Admin endpoint (/admin) returned HTTP 200"
- name: Verify admin password hash file does not exist initially
run: |
if [ -f "./secrets/admin_password_hash" ]; then
echo "❌ Admin password hash file should not exist initially"
cat ./secrets/admin_password_hash
exit 1
fi
echo "✅ Admin password hash file correctly does not exist initially"
- name: Test admin password reset flow
run: |
echo "🔧 Testing admin password reset flow..."
# Run the reset password script with auto-confirm
echo "Running reset-admin-password.sh script..."
password_output=$(docker exec aliasvault-test reset-admin-password.sh -y 2>&1)
echo "Script output:"
echo "$password_output"
# Extract the generated password from the output
generated_password=$(echo "$password_output" | grep -E "^Password: " | sed 's/Password: //')
if [ -z "$generated_password" ]; then
echo "❌ Failed to extract generated password from script output"
echo "Full output was:"
echo "$password_output"
exit 1
fi
echo "✅ Generated password extracted: $generated_password"
# Verify that the admin_password_hash file now exists in the container
if ! docker exec aliasvault-test test -f /secrets/admin_password_hash; then
echo "❌ Admin password hash file was not created in container"
docker exec aliasvault-test ls -la /secrets/
exit 1
fi
echo "✅ Admin password hash file created in container"
# Verify that the admin_password_hash file exists locally (mounted volume)
if [ ! -f "./secrets/admin_password_hash" ]; then
echo "❌ Admin password hash file not found in local secrets folder"
ls -la ./secrets/
exit 1
fi
echo "✅ Admin password hash file exists in local secrets folder"
- name: Test SMTP port
uses: nick-fields/retry@v3
with:

View File

@@ -9,31 +9,60 @@
<ServerValidationErrors @ref="ServerValidationErrors" />
<EditForm Model="Input" FormName="LoginForm" OnValidSubmit="LoginUser" class="mt-8 space-y-6">
<DataAnnotationsValidator/>
<div>
<label asp-for="Input.UserName" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your username</label>
<InputTextField id="username" @bind-Value="Input.UserName" type="text" placeholder="username" />
<ValidationMessage For="() => Input.UserName"/>
</div>
<div>
<label asp-for="Input.Password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your password</label>
<InputTextField id="password" @bind-Value="Input.Password" type="password" placeholder="••••••••" />
<ValidationMessage For="() => Input.Password"/>
</div>
<div class="flex items-start">
<div class="flex items-center h-5">
<input id="remember" aria-describedby="remember" name="remember" type="checkbox" class="w-4 h-4 border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300 dark:focus:ring-primary-600 dark:ring-offset-gray-800 dark:bg-gray-700 dark:border-gray-600">
@if (!IsAdminConfigured)
{
<div class="mt-8 p-6 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg">
<div class="flex items-start">
<div class="flex-shrink-0">
<svg class="w-5 h-5 text-yellow-600 dark:text-yellow-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-yellow-800 dark:text-yellow-200">
Admin User Not Configured
</h3>
<div class="mt-2 text-sm text-yellow-700 dark:text-yellow-300">
<p class="mb-3">The admin user has not been configured yet. To set up admin access:</p>
<ol class="list-decimal list-inside space-y-1 mb-3">
<li>Connect to your Docker container: <code class="bg-yellow-100 dark:bg-yellow-800 px-1 py-0.5 rounded text-xs">docker exec -it [container-name] /bin/bash</code></li>
<li>Run the password reset script: <code class="bg-yellow-100 dark:bg-yellow-800 px-1 py-0.5 rounded text-xs">reset-admin-password.sh</code></li>
<li>Restart the container to apply changes: <code class="bg-yellow-100 dark:bg-yellow-800 px-1 py-0.5 rounded text-xs">docker restart [container-name]</code></li>
</ol>
<p class="text-xs">Replace <code class="bg-yellow-100 dark:bg-yellow-800 px-1 py-0.5 rounded">[container-name]</code> with your actual container name, e.g. "aliasvault".</p>
</div>
</div>
</div>
<div class="ml-3 text-sm">
<label for="remember" class="font-medium text-gray-900 dark:text-white">Remember me</label>
</div>
<a href="user/forgot-password" class="ml-auto text-sm text-primary-700 hover:underline dark:text-primary-500">Lost Password?</a>
</div>
}
else
{
<EditForm Model="Input" FormName="LoginForm" OnValidSubmit="LoginUser" class="mt-8 space-y-6">
<DataAnnotationsValidator/>
<div>
<label asp-for="Input.UserName" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your username</label>
<InputTextField id="username" @bind-Value="Input.UserName" type="text" placeholder="username" />
<ValidationMessage For="() => Input.UserName"/>
</div>
<div>
<label asp-for="Input.Password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your password</label>
<InputTextField id="password" @bind-Value="Input.Password" type="password" placeholder="••••••••" />
<ValidationMessage For="() => Input.Password"/>
</div>
<button type="submit" class="w-full px-5 py-3 text-base font-medium text-center text-white bg-primary-700 rounded-lg hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 sm:w-auto dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">Login to your account</button>
</EditForm>
<div class="flex items-start">
<div class="flex items-center h-5">
<input id="remember" aria-describedby="remember" name="remember" type="checkbox" class="w-4 h-4 border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300 dark:focus:ring-primary-600 dark:ring-offset-gray-800 dark:bg-gray-700 dark:border-gray-600">
</div>
<div class="ml-3 text-sm">
<label for="remember" class="font-medium text-gray-900 dark:text-white">Remember me</label>
</div>
<a href="user/forgot-password" class="ml-auto text-sm text-primary-700 hover:underline dark:text-primary-500">Lost Password?</a>
</div>
<button type="submit" class="w-full px-5 py-3 text-base font-medium text-center text-white bg-primary-700 rounded-lg hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 sm:w-auto dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">Login to your account</button>
</EditForm>
}
@code {
@@ -43,10 +72,17 @@
[SupplyParameterFromQuery] private string? ReturnUrl { get; set; }
private bool IsAdminConfigured { get; set; } = true;
/// <inheritdoc />
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
// Check if admin user exists
var adminUser = await UserManager.FindByNameAsync("admin");
IsAdminConfigured = adminUser != null;
if (HttpMethods.IsGet(HttpContext.Request.Method))
{
// Clear the existing external cookie to ensure a clean login process

View File

@@ -34,9 +34,19 @@ builder.Services.ConfigureLogging(builder.Configuration, Assembly.GetExecutingAs
var config = new Config();
// Get admin password hash and generation timestamp using SecretReader
var (adminPasswordHash, lastPasswordChanged) = SecretReader.GetAdminPasswordHash();
config.AdminPasswordHash = adminPasswordHash;
config.LastPasswordChanged = lastPasswordChanged;
// If the admin password hash file doesn't exist, leave config values empty (admin user won't be created)
try
{
var (adminPasswordHash, lastPasswordChanged) = SecretReader.GetAdminPasswordHash();
config.AdminPasswordHash = adminPasswordHash;
config.LastPasswordChanged = lastPasswordChanged;
}
catch (KeyNotFoundException)
{
// Admin password hash not configured - this is expected when no password has been set yet
config.AdminPasswordHash = string.Empty;
config.LastPasswordChanged = DateTime.MinValue;
}
var ipLoggingEnabled = Environment.GetEnvironmentVariable("IP_LOGGING_ENABLED") ?? "false";
config.IpLoggingEnabled = bool.Parse(ipLoggingEnabled);

View File

@@ -33,7 +33,7 @@ public static class StartupTasks
}
/// <summary>
/// Creates the admin user if it does not exist.
/// Creates the admin user if it does not exist and admin password hash is configured.
/// </summary>
/// <param name="serviceProvider">IServiceProvider instance.</param>
/// <returns>Async Task.</returns>
@@ -44,6 +44,14 @@ public static class StartupTasks
var adminUser = await userManager.FindByNameAsync("admin");
var config = serviceProvider.GetRequiredService<Config>();
// Skip admin user creation if no admin password hash is configured
if (string.IsNullOrEmpty(config.AdminPasswordHash))
{
Console.WriteLine("Admin password hash not configured - skipping admin user creation.");
Console.WriteLine("Run 'reset-admin-password.sh' to configure the admin password.");
return;
}
if (adminUser == null)
{
var adminPasswordHash = config.AdminPasswordHash;
@@ -59,9 +67,9 @@ public static class StartupTasks
}
else
{
// Check if the password hash is different AND the password in .env file is newer than the password of user.
// If so, update the password hash of the user in the database so it matches the one in the .env file.
if (adminUser.PasswordHash != config.AdminPasswordHash && (adminUser.LastPasswordChanged is null || config.LastPasswordChanged > adminUser.LastPasswordChanged))
// Check if the password hash is different AND the hash in secret file is newer than the password of user.
// If so, update the password hash of the user in the database so it matches the one in the admin_password_hash file.
if (adminUser.PasswordHash != config.AdminPasswordHash && (adminUser.LastPasswordChanged is null || (config.LastPasswordChanged != DateTime.MinValue && config.LastPasswordChanged > adminUser.LastPasswordChanged)))
{
// The password has been changed in the .env file, update the user's password hash.
adminUser.PasswordHash = config.AdminPasswordHash;

View File

@@ -554,6 +554,40 @@ video {
--tw-contain-style: ;
}
.container {
width: 100%;
}
@media (min-width: 640px) {
.container {
max-width: 640px;
}
}
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
@media (min-width: 1024px) {
.container {
max-width: 1024px;
}
}
@media (min-width: 1280px) {
.container {
max-width: 1280px;
}
}
@media (min-width: 1536px) {
.container {
max-width: 1536px;
}
}
.sr-only {
position: absolute;
width: 1px;
@@ -978,6 +1012,10 @@ video {
cursor: pointer;
}
.list-inside {
list-style-position: inside;
}
.list-decimal {
list-style-type: decimal;
}
@@ -1245,6 +1283,11 @@ video {
border-color: rgb(234 179 8 / var(--tw-border-opacity));
}
.border-yellow-200 {
--tw-border-opacity: 1;
border-color: rgb(254 240 138 / var(--tw-border-opacity));
}
.bg-blue-100 {
--tw-bg-opacity: 1;
background-color: rgb(219 234 254 / var(--tw-bg-opacity));
@@ -1794,6 +1837,11 @@ video {
color: rgb(133 77 14 / var(--tw-text-opacity));
}
.text-yellow-600 {
--tw-text-opacity: 1;
color: rgb(202 138 4 / var(--tw-text-opacity));
}
.underline {
text-decoration-line: underline;
}
@@ -2092,6 +2140,11 @@ video {
border-color: rgb(234 179 8 / var(--tw-border-opacity));
}
.dark\:border-yellow-800:is(.dark *) {
--tw-border-opacity: 1;
border-color: rgb(133 77 14 / var(--tw-border-opacity));
}
.dark\:bg-blue-800:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(30 64 175 / var(--tw-bg-opacity));
@@ -2183,6 +2236,10 @@ video {
background-color: rgb(113 63 18 / var(--tw-bg-opacity));
}
.dark\:bg-yellow-900\/20:is(.dark *) {
background-color: rgb(113 63 18 / 0.2);
}
.dark\:bg-opacity-80:is(.dark *) {
--tw-bg-opacity: 0.8;
}
@@ -2282,6 +2339,16 @@ video {
color: rgb(254 240 138 / var(--tw-text-opacity));
}
.dark\:text-yellow-300:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(253 224 71 / var(--tw-text-opacity));
}
.dark\:text-yellow-400:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(250 204 21 / var(--tw-text-opacity));
}
.dark\:placeholder-gray-400:is(.dark *)::-moz-placeholder {
--tw-placeholder-opacity: 1;
color: rgb(156 163 175 / var(--tw-placeholder-opacity));

View File

@@ -174,10 +174,6 @@ public class UserManagementTests : AdminPlaywrightTest
// Wait for the user details page to load
await WaitForUrlAsync($"users/{_testUserId}**", _testUserEmail);
// Verify we're on the correct user's page
var pageContent = await Page.TextContentAsync("body");
Assert.That(pageContent, Does.Contain(_testUserEmail), "Test user email should be visible on the user details page");
// Click the edit username button (the SVG edit icon)
var editButton = Page.Locator("button[id='edit-username-button']");
await editButton.ClickAsync();
@@ -186,7 +182,7 @@ public class UserManagementTests : AdminPlaywrightTest
await Page.WaitForSelectorAsync("text=Change Username");
// Verify the form appeared
pageContent = await Page.TextContentAsync("body");
var pageContent = await Page.TextContentAsync("body");
Assert.That(pageContent, Does.Contain("Change Username"), "Username change form should appear after clicking edit button");
Assert.That(pageContent, Does.Contain("Changing a username is permanent"), "Warning message should be visible");

View File

@@ -106,6 +106,10 @@ COPY --from=dotnet-builder /app/installcli /usr/local/bin/aliasvault-cli
RUN chmod +x /usr/local/bin/aliasvault-cli/AliasVault.InstallCli && \
ln -s /usr/local/bin/aliasvault-cli/AliasVault.InstallCli /usr/local/bin/aliasvault-cli.sh
# Copy password reset script and make it executable
COPY dockerfiles/all-in-one/reset-admin-password.sh /usr/local/bin/reset-admin-password.sh
RUN chmod +x /usr/local/bin/reset-admin-password.sh
# Copy client nginx configuration and ensure wwwroot is accessible
COPY apps/server/AliasVault.Client/nginx.conf /app/client/nginx.conf

View File

@@ -0,0 +1,182 @@
#!/bin/bash
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Default values
CONFIRM_RESET=false
PASSWORD_LENGTH=16
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-y|--yes)
CONFIRM_RESET=true
shift
;;
-l|--length)
PASSWORD_LENGTH="$2"
shift 2
;;
-h|--help)
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Reset the admin password with a randomly generated password"
echo ""
echo "OPTIONS:"
echo " -y, --yes Skip confirmation prompt"
echo " -l, --length NUM Password length (default: 16)"
echo " -h, --help Show this help message"
exit 0
;;
*)
echo -e "${RED}Unknown option: $1${NC}"
echo "Use -h or --help for usage information"
exit 1
;;
esac
done
# Function to generate a secure random password
generate_password() {
local length=$1
# Generate password with uppercase, lowercase, numbers, and special characters
# Using /dev/urandom for cryptographically secure randomness
local password=$(tr -dc 'A-Za-z0-9!@#$%^&*()_+=-' < /dev/urandom | head -c "$length")
echo "$password"
}
# Function to hash the password using the aliasvault-cli
hash_password() {
local password=$1
local hash
# Check if aliasvault-cli.sh exists
if [ ! -f /usr/local/bin/aliasvault-cli.sh ] && [ ! -L /usr/local/bin/aliasvault-cli.sh ]; then
echo -e "${RED}Error: aliasvault-cli.sh not found${NC}" >&2
return 1
fi
# Hash the password
hash=$(/usr/local/bin/aliasvault-cli.sh hash-password "$password" 2>/dev/null)
if [ $? -ne 0 ] || [ -z "$hash" ]; then
echo -e "${RED}Error: Failed to hash password${NC}" >&2
return 1
fi
echo "$hash"
}
# Function to update the admin password hash file
update_hash_file() {
local hash=$1
local hash_file="/secrets/admin_password_hash"
# Create /secrets directory if it doesn't exist
if [ ! -d "/secrets" ]; then
mkdir -p /secrets
if [ $? -ne 0 ]; then
echo -e "${RED}Error: Failed to create /secrets directory${NC}" >&2
return 1
fi
fi
# Get current timestamp in ISO8601 format (UTC)
local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
# Write hash and timestamp to file
cat > "$hash_file" <<EOF
$hash|$timestamp
EOF
if [ $? -ne 0 ]; then
echo -e "${RED}Error: Failed to write hash to $hash_file${NC}" >&2
return 1
fi
# Set appropriate permissions (readable by the application, not world-readable)
chmod 600 "$hash_file"
echo -e "${GREEN}Password hash updated successfully${NC}"
echo -e "Hash file: $hash_file"
echo -e "Updated at: $timestamp"
return 0
}
# Main execution
main() {
echo -e "${YELLOW}=== AliasVault Admin Password Reset ===${NC}"
echo ""
# Check if running in Docker container
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then
echo -e "${YELLOW}Warning: This script appears to be running outside of a Docker container${NC}"
echo -e "${YELLOW}The password hash file will be created at: /secrets/admin_password_hash${NC}"
echo ""
fi
# Confirmation prompt
if [ "$CONFIRM_RESET" = false ]; then
echo -e "${YELLOW}This will reset the admin password with a new randomly generated password.${NC}"
echo -e "${YELLOW}The current admin password (if any) will be permanently overwritten.${NC}"
echo ""
read -p "Are you sure you want to reset the admin password? (yes/no): " confirm
if [[ ! "$confirm" =~ ^[Yy]([Ee][Ss])?$ ]]; then
echo -e "${RED}Password reset cancelled${NC}"
exit 0
fi
fi
echo ""
echo "Generating new password..."
# Generate random password
NEW_PASSWORD=$(generate_password "$PASSWORD_LENGTH")
if [ -z "$NEW_PASSWORD" ]; then
echo -e "${RED}Error: Failed to generate password${NC}"
exit 1
fi
# Hash the password
PASSWORD_HASH=$(hash_password "$NEW_PASSWORD")
if [ $? -ne 0 ] || [ -z "$PASSWORD_HASH" ]; then
echo -e "${RED}Error: Failed to hash password${NC}"
exit 1
fi
# Update the hash file
update_hash_file "$PASSWORD_HASH"
if [ $? -ne 0 ]; then
echo -e "${RED}Error: Failed to update password hash file${NC}"
exit 1
fi
echo ""
echo -e "${GREEN}========================================${NC}"
echo -e "${GREEN}Admin password reset successful!${NC}"
echo -e "${GREEN}========================================${NC}"
echo ""
echo -e "${YELLOW}New Admin Credentials:${NC}"
echo -e "Username: ${GREEN}admin${NC}"
echo -e "Password: ${GREEN}$NEW_PASSWORD${NC}"
echo ""
echo -e "${YELLOW}IMPORTANT:${NC}"
echo -e "1. Save this password securely - it will not be shown again"
echo -e "2. The password hash has been saved to /secrets/admin_password_hash"
echo -e "3. Restart the Docker container for the new password to take effect"
echo ""
exit 0
}
# Run main function
main

View File

@@ -62,33 +62,9 @@ else
log 0 "[init] ✅ JWT key already exists"
fi
if [ ! -f /secrets/admin_password_hash ]; then
log 0 "[init] → Generating admin password hash..."
# Generate a random admin password if not provided
if [ -z "$ADMIN_PASSWORD" ]; then
ADMIN_PASSWORD=$(openssl rand -base64 16 | tr -d "\n")
# Only show password in verbose mode during init
if [ "$VERBOSITY" -ge 2 ]; then
echo "[init] ⚠️ Generated random admin password: $ADMIN_PASSWORD"
echo "[init] ⚠️ Please save this password securely and/or optionally change it after first login!"
fi
# Save the password temporarily for the final notification (only if newly generated)
echo "$ADMIN_PASSWORD" > /secrets/admin_password_temp
chmod 600 /secrets/admin_password_temp
fi
# Use the InstallCLI to hash the password and append generation timestamp
if [ "$VERBOSITY" -ge 2 ]; then
HASH=$(/usr/local/bin/aliasvault-cli/AliasVault.InstallCli hash-password "$ADMIN_PASSWORD")
else
HASH=$(/usr/local/bin/aliasvault-cli/AliasVault.InstallCli hash-password "$ADMIN_PASSWORD" 2>/dev/null)
fi
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
echo "${HASH}|${TIMESTAMP}" > /secrets/admin_password_hash
chmod 600 /secrets/admin_password_hash
log 0 "[init] Admin password hash saved to /secrets/admin_password_hash"
else
log 0 "[init] ✅ Admin password hash already exists"
fi
# Admin password is not created by default
# Users must run reset-admin-password.sh to configure the admin password
log 0 "[init] ✅ Admin password configuration deferred to manual setup"
# Read PostgreSQL password for database initialization
POSTGRES_PASSWORD=$(cat /secrets/postgres_password)

View File

@@ -32,22 +32,21 @@ if [ "$VERBOSITY" -le 1 ]; then
echo " • Admin: https://localhost:443/admin"
echo ""
# Show admin credentials if available
if [ -f /secrets/admin_password_temp ]; then
ADMIN_PASSWORD=$(cat /secrets/admin_password_temp)
# Check if admin password hash file exists to determine which message to show
if [ -f /secrets/admin_password_hash ]; then
# Admin password hash exists - show the legacy warning
echo "🔑 Admin Login:"
echo " • Username: admin"
echo " • Password: ${ADMIN_PASSWORD}"
echo " • Password: (previously set - to reset the admin password, login to this container via \`docker exec -it [container-name] /bin/bash\` and run: reset-admin-password.sh)"
echo ""
echo "⚠️ IMPORTANT: Save these credentials securely!"
echo " This password won't be shown again."
echo ""
# Clean up the temporary password file
rm -f /secrets/admin_password_temp
else
echo "🔑 Admin Login:"
echo " • Username: admin"
echo " • Password: (previously set - to reset the admin password, delete the file ./secrets/admin_password_hash and restart the container)"
# No admin password hash file - show setup instructions
echo "🔑 Admin Setup:"
echo " • Admin user is not configured by default"
echo " • To configure admin access:"
echo " 1. docker exec -it [container-name] /bin/bash"
echo " 2. reset-admin-password.sh"
echo " 3. Restart the container"
echo ""
fi