diff --git a/.env.example b/.env.example index ca1cb3fbe..4984f71e3 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -API_URL= +HOSTNAME= JWT_KEY= DATA_PROTECTION_CERT_PASS= ADMIN_PASSWORD_HASH= diff --git a/.github/workflows/docker-compose-build.yml b/.github/workflows/docker-compose-build.yml index 85656da2f..8f81ebe32 100644 --- a/.github/workflows/docker-compose-build.yml +++ b/.github/workflows/docker-compose-build.yml @@ -32,29 +32,43 @@ jobs: run: | # Wait for a few seconds sleep 10 - - name: Test if localhost:80 (WASM app) responds + - name: Test if localhost:443 (WASM app) responds uses: nick-fields/retry@v3 with: timeout_minutes: 2 max_attempts: 3 command: | - http_code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:80) + http_code=$(curl -k -s -o /dev/null -w "%{http_code}" https://localhost:443) if [ "$http_code" -ne 200 ]; then - echo "Service did not respond with 200 OK. Check if client app is configured correctly." + echo "Service did not respond with 200 OK. Check if client app and/or nginx is configured correctly." exit 1 else echo "Service responded with 200 OK" fi - - name: Test if localhost:81 (WebApi) responds + - name: Test if localhost:443/api (WebApi) responds uses: nick-fields/retry@v3 with: timeout_minutes: 2 max_attempts: 3 command: | - http_code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:81) + http_code=$(curl -k -s -o /dev/null -w "%{http_code}" https://localhost:443/api) if [ "$http_code" -ne 200 ]; then - echo "Service did not respond with expected 200 OK. Check if WebApi is configured correctly." + echo "Service did not respond with expected 200 OK. Check if WebApi and/or nginx is configured correctly." + exit 1 + else + echo "Service responded with $http_code" + fi + + - name: Test if localhost:443/admin (Admin) responds + uses: nick-fields/retry@v3 + with: + timeout_minutes: 2 + max_attempts: 3 + command: | + http_code=$(curl -k -s -o /dev/null -w "%{http_code}" https://localhost:443/admin/user/login) + if [ "$http_code" -ne 200 ]; then + echo "Service did not respond with expected 200 OK. Check if admin app and/or nginx is configured correctly." exit 1 else echo "Service responded with $http_code" @@ -73,16 +87,13 @@ jobs: echo "SmtpService responded on port 2525" fi - - name: Test if localhost:8080 (Admin) responds - uses: nick-fields/retry@v3 - with: - timeout_minutes: 2 - max_attempts: 3 - command: | - http_code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/user/login) - if [ "$http_code" -ne 200 ]; then - echo "Service did not respond with expected 200 OK. Check if admin app is configured correctly." - exit 1 - else - echo "Service responded with $http_code" - fi + - name: Test install.sh --reset-password output + run: | + output=$(./install.sh --reset-password) + if ! echo "$output" | grep -E '^Password: [a-zA-Z0-9]{8,}$'; then + echo "Password reset output format is incorrect. Expected format: 'Password: '" + echo "Actual output: $output" + exit 1 + else + echo "Password reset output format is correct" + fi diff --git a/.gitignore b/.gitignore index dc89dca9d..65dce725a 100644 --- a/.gitignore +++ b/.gitignore @@ -393,3 +393,8 @@ src/Tests/AliasVault.E2ETests/appsettings.Development.json # Draw.io diagram temp files *.drawio.* +# Certificates +certificates/**/*.crt +certificates/**/*.key +certificates/**/*.pfx +certificates/**/*.pem diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..1d37a84c6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM nginx:alpine + +# Install OpenSSL for certificate generation +RUN apk add --no-cache openssl + +# Copy configuration and entrypoint script +COPY nginx.conf /etc/nginx/nginx.conf +COPY entrypoint.sh /docker-entrypoint.sh + +# Create SSL directory +RUN mkdir -p /etc/nginx/ssl && chmod 755 /etc/nginx/ssl \ + && chmod +x /docker-entrypoint.sh + +EXPOSE 80 443 +ENTRYPOINT ["/docker-entrypoint.sh"] diff --git a/README.md b/README.md index 06b02b43c..3f5e90303 100644 --- a/README.md +++ b/README.md @@ -59,9 +59,9 @@ $ chmod +x install.sh && ./install.sh Note: if you do not wish to run the script, you can set up the environment variables and build the Docker image and containers manually instead. See the [manual setup instructions](docs/install/1-manually-setup-docker.md) for more information. ### 2. Ready to use -The install script executed in step #1 will output the URL where the app is available. By default this is http://localhost:80 for the client and http://localhost:8080 for the admin. +The install script executed in step #1 will output the URL where the app is available. By default this is https://localhost for the client and https://localhost/admin for the admin portal. -> Note: If you want to change the default AliasVault ports you can do so in the `docker-compose.yml` file. +> Note: If you want to change the default AliasVault ports you can do so in the `docker-compose.yml` file for the `nginx` (reverse-proxy) container. #### Note for first time build: - When running the init script for the first time, it may take a few minutes for Docker to download all dependencies. Subsequent builds will be faster. diff --git a/certificates/README.md b/certificates/README.md index 007cb45fa..b15837c13 100644 --- a/certificates/README.md +++ b/certificates/README.md @@ -1,4 +1,6 @@ -This is the default location where (self-generated) certificates are stored. +# Certificates directory structure -For example, the API and Admin projects make use of the .NET DataProtection API that depends on -certificates for encrypting various types of application data such as authentication cookies, anti-forgery tokens etc. +This directory contains certificates for AliasVault. + +- `app`: Certificates that AliasVault uses to protect application data at rest (e.g. .NET DataProtection keys) +- `ssl`: SSL/TLS certificates for AliasVault hosted services diff --git a/certificates/ssl/README.md b/certificates/ssl/README.md new file mode 100644 index 000000000..d6b748e4e --- /dev/null +++ b/certificates/ssl/README.md @@ -0,0 +1,7 @@ +# SSL certificates directory structure + +This directory contains SSL/TLS certificates for various AliasVault services: + +- `admin`: Certificate for the Admin UI. +- `api`: Certificate for the API service. +- `client`: Certificate for the Client UI. diff --git a/docker-compose.yml b/docker-compose.yml index eb31509ac..e870a1ab2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,25 +1,29 @@ services: - admin: - image: aliasvault-admin + nginx: build: context: . - dockerfile: src/AliasVault.Admin/Dockerfile + dockerfile: Dockerfile ports: - - "8080:8082" + - "80:80" + - "443:443" volumes: - - ./certificates:/certificates:rw - - ./database:/database:rw - - ./logs:/logs:rw + - ./certificates/ssl:/etc/nginx/ssl:rw + depends_on: + - admin + - client + - api + - smtp restart: always - env_file: - - .env + client: image: aliasvault-client build: context: . dockerfile: src/AliasVault.Client/Dockerfile - ports: - - "80:8080" + volumes: + - ./logs/msbuild:/src/msbuild-logs:rw + expose: + - "3000" restart: always env_file: - .env @@ -29,16 +33,31 @@ services: build: context: . dockerfile: src/AliasVault.Api/Dockerfile - ports: - - "81:8081" + expose: + - "3001" volumes: - - ./certificates:/certificates:rw - ./database:/database:rw + - ./certificates/app:/certificates/app:rw - ./logs:/logs:rw env_file: - .env restart: always + admin: + image: aliasvault-admin + build: + context: . + dockerfile: src/AliasVault.Admin/Dockerfile + expose: + - "3002" + volumes: + - ./database:/database:rw + - ./certificates/app:/certificates/app:rw + - ./logs:/logs:rw + restart: always + env_file: + - .env + smtp: image: aliasvault-smtp build: diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 000000000..4fb1e2875 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +# Create SSL directory if it doesn't exist +mkdir -p /etc/nginx/ssl + +# Generate self-signed SSL certificate if not exists +if [ ! -f /etc/nginx/ssl/cert.pem ] || [ ! -f /etc/nginx/ssl/key.pem ]; then + echo "Generating new SSL certificate..." + openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ + -keyout /etc/nginx/ssl/key.pem \ + -out /etc/nginx/ssl/cert.pem \ + -subj "/C=US/ST=State/L=City/O=Organization/CN=localhost" + + # Set proper permissions + chmod 644 /etc/nginx/ssl/cert.pem + chmod 600 /etc/nginx/ssl/key.pem +fi + +# Start nginx +nginx -g "daemon off;" \ No newline at end of file diff --git a/install.sh b/install.sh index 955097506..72ef220cd 100755 --- a/install.sh +++ b/install.sh @@ -140,22 +140,22 @@ create_env_file() { fi } -# Function to check and populate the .env file with API_URL -populate_api_url() { - printf "${CYAN}> Checking API_URL...${NC}\n" - if ! grep -q "^API_URL=" "$ENV_FILE" || [ -z "$(grep "^API_URL=" "$ENV_FILE" | cut -d '=' -f2)" ]; then - DEFAULT_API_URL="http://localhost:81" - read -p "Enter the base URL where the API will be hosted (press Enter for default: $DEFAULT_API_URL): " USER_API_URL - API_URL=${USER_API_URL:-$DEFAULT_API_URL} - if grep -q "^API_URL=" "$ENV_FILE"; then - awk -v url="$API_URL" '/^API_URL=/ {$0="API_URL="url} 1' "$ENV_FILE" > "$ENV_FILE.tmp" && mv "$ENV_FILE.tmp" "$ENV_FILE" +# Function to populate HOSTNAME +populate_hostname() { + printf "${CYAN}> Checking HOSTNAME...${NC}\n" + if ! grep -q "^HOSTNAME=" "$ENV_FILE" || [ -z "$(grep "^HOSTNAME=" "$ENV_FILE" | cut -d '=' -f2)" ]; then + DEFAULT_HOSTNAME="localhost" + read -p "Enter the hostname where AliasVault will be hosted (press Enter for default: $DEFAULT_HOSTNAME): " USER_HOSTNAME + HOSTNAME=${USER_HOSTNAME:-$DEFAULT_HOSTNAME} + if grep -q "^HOSTNAME=" "$ENV_FILE"; then + awk -v hostname="$HOSTNAME" '/^HOSTNAME=/ {$0="HOSTNAME="hostname} 1' "$ENV_FILE" > "$ENV_FILE.tmp" && mv "$ENV_FILE.tmp" "$ENV_FILE" else - echo "API_URL=${API_URL}" >> "$ENV_FILE" + echo "HOSTNAME=${HOSTNAME}" >> "$ENV_FILE" fi - printf "${GREEN}> API_URL has been set to $API_URL in $ENV_FILE.${NC}\n" + printf "${GREEN}> HOSTNAME has been set to $HOSTNAME in $ENV_FILE.${NC}\n" else - API_URL=$(grep "^API_URL=" "$ENV_FILE" | cut -d '=' -f2) - printf "${GREEN}> API_URL already exists in $ENV_FILE with value: $API_URL${NC}\n" + HOSTNAME=$(grep "^HOSTNAME=" "$ENV_FILE" | cut -d '=' -f2) + printf "${GREEN}> HOSTNAME already exists in $ENV_FILE with value: $HOSTNAME${NC}\n" fi } @@ -359,7 +359,7 @@ main() { printf "${YELLOW}+++ Initializing .env file +++${NC}\n" printf "\n" create_env_file || exit $? - populate_api_url || exit $? + populate_hostname || exit $? populate_jwt_key || exit $? populate_data_protection_cert_pass || exit $? set_private_email_domains || exit $? @@ -381,14 +381,14 @@ main() { printf "${CYAN}To configure the server, login to the admin panel:${NC}\n" printf "\n" if [ "$ADMIN_PASSWORD" != "" ]; then - printf "Admin Panel: http://localhost:8080/\n" + printf "Admin Panel: https://$HOSTNAME/admin\n" printf "Username: admin\n" printf "Password: $ADMIN_PASSWORD\n" printf "\n" printf "${YELLOW}(!) Caution: Make sure to backup the above credentials in a safe place, they won't be shown again!${NC}\n" printf "\n" else - printf "Admin Panel: http://localhost:8080/\n" + printf "Admin Panel: https://$HOSTNAME/admin\n" printf "Username: admin\n" printf "Password: (Previously set. Run this command with --reset-password to generate a new one.)\n" printf "\n" @@ -397,8 +397,7 @@ main() { printf "\n" printf "${CYAN}In order to start using AliasVault and create your own vault, log into the client website:${NC}\n" printf "\n" - printf "Client Website: http://localhost:80/\n" - printf "You can create your own account from there.\n" + printf "Client Website: https://$HOSTNAME/\n" printf "\n" printf "${MAGENTA}=========================================================${NC}\n" } diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 000000000..d6b5c7e0f --- /dev/null +++ b/nginx.conf @@ -0,0 +1,85 @@ +events { + worker_connections 1024; +} + +http { + upstream client { + server client:3000; + } + + upstream api { + server api:3001; + } + + upstream admin { + server admin:3002; + } + + # Preserve any existing X-Forwarded-* headers, this is relevant if AliasVault + # is running behind another reverse proxy. + set_real_ip_from 10.0.0.0/8; + set_real_ip_from 172.16.0.0/12; + set_real_ip_from 192.168.0.0/16; + real_ip_header X-Forwarded-For; + real_ip_recursive on; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Enable gzip compression, which reduces the amount of data that needs to be transferred + # to speed up WASM load times. + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_buffers 16 8k; + gzip_http_version 1.1; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + + server { + listen 80; + server_name _; + return 301 https://$host$request_uri; + } + + server { + listen 443 ssl; + server_name _; + + ssl_certificate /etc/nginx/ssl/cert.pem; + ssl_certificate_key /etc/nginx/ssl/key.pem; + + # Client app (root path) + location / { + proxy_pass http://client; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + # Admin interface + location /admin { + proxy_pass http://admin; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + # Add WebSocket support for Blazor server + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_read_timeout 86400; + } + + # API endpoints + location /api { + proxy_pass http://api; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + } +} \ No newline at end of file diff --git a/src/AliasVault.Admin/Auth/Components/Logo.razor b/src/AliasVault.Admin/Auth/Components/Logo.razor index af5733c11..c3eb1e7dd 100644 --- a/src/AliasVault.Admin/Auth/Components/Logo.razor +++ b/src/AliasVault.Admin/Auth/Components/Logo.razor @@ -1,9 +1,10 @@ - +@using AliasVault.Admin.Services +@inject NavigationService NavigationService + +
AliasVault AliasVault
-
- - + \ No newline at end of file diff --git a/src/AliasVault.Admin/Dockerfile b/src/AliasVault.Admin/Dockerfile index cbbc3c3b1..65dcfbb13 100644 --- a/src/AliasVault.Admin/Dockerfile +++ b/src/AliasVault.Admin/Dockerfile @@ -1,22 +1,18 @@ FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base WORKDIR /app -EXPOSE 8082 +EXPOSE 3002 FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build ARG BUILD_CONFIGURATION=Release WORKDIR /src - -# Copy the project files and restore dependencies COPY ["src/AliasVault.Admin/AliasVault.Admin.csproj", "src/AliasVault.Admin/"] RUN dotnet restore "src/AliasVault.Admin/AliasVault.Admin.csproj" COPY . . -# Build the WebApi project WORKDIR "/src/src/AliasVault.Admin" RUN dotnet build "AliasVault.Admin.csproj" -c "$BUILD_CONFIGURATION" -o /app/build -# Publish the application to the /app/publish directory in the container FROM build AS publish ARG BUILD_CONFIGURATION=Release RUN dotnet publish "AliasVault.Admin.csproj" -c "$BUILD_CONFIGURATION" -o /app/publish /p:UseAppHost=false @@ -24,6 +20,7 @@ RUN dotnet publish "AliasVault.Admin.csproj" -c "$BUILD_CONFIGURATION" -o /app/p FROM base AS final WORKDIR /app COPY --from=publish /app/publish . -EXPOSE 8082 -ENV ASPNETCORE_URLS=http://+:8082 + +ENV ASPNETCORE_URLS=http://+:3002 +ENV ASPNETCORE_PATHBASE=/admin ENTRYPOINT ["dotnet", "AliasVault.Admin.dll"] diff --git a/src/AliasVault.Admin/Main/App.razor b/src/AliasVault.Admin/Main/App.razor index 0ab53e4cd..bd46984a5 100644 --- a/src/AliasVault.Admin/Main/App.razor +++ b/src/AliasVault.Admin/Main/App.razor @@ -5,7 +5,7 @@ - + diff --git a/src/AliasVault.Admin/Main/Layout/TopMenu.razor b/src/AliasVault.Admin/Main/Layout/TopMenu.razor index 496b41fd5..e76379b53 100644 --- a/src/AliasVault.Admin/Main/Layout/TopMenu.razor +++ b/src/AliasVault.Admin/Main/Layout/TopMenu.razor @@ -5,7 +5,7 @@