Add LetsEncrypt ssl certificate generation to docker setup (#388)

* Add LetsEncrypt scaffolding to docker compose setup (#367)

* Update install.sh (#367)

* Add certificate request logic (#367)

* Update domain validation regex (#367)

* Update install.sh (#367)

* Update install.sh (#367)

* Update nginx.conf for LetsEncrypt validation (#367)

* Update nginx.conf (#367)

* Add certbot volume mapping to nginx (#367)

* Update nginx conf to template to use env vars (#367)

* Update nginx certbot root (#367)

* Update install.sh (#367)

* Update nginx ssl letsencrypt paths (#367)

* Update install.sh (#367)

* Use conditional nginx.conf include instead of vars (#367)

* Update install.sh so it doesn't restart docker stack but expects it to be running already (#367)

* Update permissions (#367)

* Update install.sh (#367)

* Refactor and cleanup (#367)
This commit is contained in:
Leendert de Borst
2024-11-20 16:25:35 +01:00
committed by GitHub
parent 0f377bdec6
commit dfdf4981cb
8 changed files with 275 additions and 26 deletions

View File

@@ -5,3 +5,4 @@ ADMIN_PASSWORD_HASH=
ADMIN_PASSWORD_GENERATED=2024-01-01T00:00:00Z
PRIVATE_EMAIL_DOMAINS=
SMTP_TLS_ENABLED=false
LETSENCRYPT_ENABLED=false

1
.gitignore vendored
View File

@@ -398,3 +398,4 @@ certificates/**/*.crt
certificates/**/*.key
certificates/**/*.pfx
certificates/**/*.pem
certificates/letsencrypt/**

View File

@@ -1,6 +1,6 @@
FROM nginx:alpine
# Install OpenSSL for certificate generation
# Install OpenSSL
RUN apk add --no-cache openssl
# Copy configuration and entrypoint script

View File

@@ -0,0 +1,7 @@
services:
certbot:
image: certbot/certbot
volumes:
- ./certificates/letsencrypt:/etc/letsencrypt:rw
- ./certificates/letsencrypt/www:/var/www/certbot:rw
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"

View File

@@ -6,12 +6,16 @@ services:
- "443:443"
volumes:
- ./certificates/ssl:/etc/nginx/ssl:rw
- ./certificates/letsencrypt:/etc/nginx/ssl-letsencrypt:rw
- ./certificates/letsencrypt/www:/var/www/certbot:rw
depends_on:
- admin
- client
- api
- smtp
restart: always
env_file:
- .env
client:
image: ghcr.io/lanedirt/aliasvault-client:latest
@@ -58,3 +62,7 @@ services:
env_file:
- .env
restart: always
networks:
aliasvault:
name: aliasvault_default

View File

@@ -16,5 +16,18 @@ if [ ! -f /etc/nginx/ssl/cert.pem ] || [ ! -f /etc/nginx/ssl/key.pem ]; then
chmod 600 /etc/nginx/ssl/key.pem
fi
# Create the appropriate SSL configuration based on LETSENCRYPT_ENABLED
if [ "${LETSENCRYPT_ENABLED}" = "true" ]; then
cat > /etc/nginx/ssl.conf << EOF
ssl_certificate /etc/nginx/ssl-letsencrypt/live/${HOSTNAME}/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl-letsencrypt/live/${HOSTNAME}/privkey.pem;
EOF
else
cat > /etc/nginx/ssl.conf << EOF
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
EOF
fi
# Start nginx
nginx -g "daemon off;"

View File

@@ -11,6 +11,8 @@ GITHUB_CONTAINER_REGISTRY="ghcr.io/$(echo "$REPO_OWNER" | tr '[:upper:]' '[:lowe
REQUIRED_DIRS=(
"certificates/ssl"
"certificates/app"
"certificates/letsencrypt"
"certificates/letsencrypt/www"
"database"
"logs"
"logs/msbuild"
@@ -35,15 +37,16 @@ show_usage() {
printf "Usage: $0 [COMMAND] [OPTIONS]\n"
printf "\n"
printf "Commands:\n"
printf " /install Install AliasVault by pulling pre-built images from GitHub Container Registry (default)\n"
printf " /build Build AliasVault from source (takes longer and requires sufficient specs)\n"
printf " /reset-password Reset admin password\n"
printf " /uninstall Uninstall AliasVault\n"
printf " install Install AliasVault by pulling pre-built images from GitHub Container Registry (default)\n"
printf " build Build AliasVault from source (takes longer and requires sufficient specs)\n"
printf " reset-password Reset admin password\n"
printf " uninstall Uninstall AliasVault\n"
printf " configure-ssl Configure SSL certificates (Let's Encrypt or self-signed)\n"
printf "\n"
printf "Options:\n"
printf " --verbose Show detailed output\n"
printf " -y, --yes Automatic yes to prompts (for uninstall)\n"
printf " -y, --yes Automatic yes to prompts (for uninstall)\n"
printf " --help Show this help message\n"
}
@@ -77,6 +80,10 @@ parse_args() {
COMMAND="reset-password"
shift
;;
configure-ssl|ssl)
COMMAND="configure-ssl"
shift
;;
--verbose)
VERBOSE=true
shift
@@ -126,6 +133,9 @@ main() {
print_password_reset_message
fi
;;
"configure-ssl")
handle_ssl_configuration
;;
esac
}
@@ -141,6 +151,7 @@ create_directories() {
dirs_needed=true
fi
mkdir -p "$dir"
chmod -R 755 "$dir"
if [ $? -ne 0 ]; then
printf " ${RED}> Failed to create directory: $dir${NC}\n"
exit 1
@@ -392,6 +403,23 @@ print_password_reset_message() {
printf "\n"
}
# Function to get docker compose command with appropriate config files
get_docker_compose_command() {
local base_command="docker compose -f docker-compose.yml"
# Check if using build configuration
if [ "$1" = "build" ]; then
base_command="$base_command -f docker-compose.build.yml"
fi
# Check if Let's Encrypt is enabled
if grep -q "^LETSENCRYPT_ENABLED=true" "$ENV_FILE" 2>/dev/null; then
base_command="$base_command -f docker-compose.letsencrypt.yml"
fi
echo "$base_command"
}
# Function to handle installation
handle_install() {
printf "${YELLOW}+++ Installing AliasVault +++${NC}\n"
@@ -439,9 +467,9 @@ handle_install() {
printf "\n${YELLOW}+++ Starting services +++${NC}\n"
printf "\n"
if [ "$VERBOSE" = true ]; then
docker compose up -d || { printf "${RED}> Failed to start Docker containers${NC}\n"; exit 1; }
$(get_docker_compose_command) up -d || { printf "${RED}> Failed to start Docker containers${NC}\n"; exit 1; }
else
docker compose up -d > /dev/null 2>&1 || { printf "${RED}> Failed to start Docker containers${NC}\n"; exit 1; }
$(get_docker_compose_command) up -d > /dev/null 2>&1 || { printf "${RED}> Failed to start Docker containers${NC}\n"; exit 1; }
fi
# Only show success message if we made it here without errors
@@ -487,13 +515,13 @@ handle_build() {
printf "${CYAN}> Building Docker Compose stack...${NC}"
if [ "$VERBOSE" = true ]; then
docker compose -f docker-compose.yml -f docker-compose.build.yml build || {
$(get_docker_compose_command "build") build || {
printf "\n${RED}> Failed to build Docker Compose stack${NC}\n"
exit 1
}
else
(
docker compose -f docker-compose.yml -f docker-compose.build.yml build > install_compose_build_output.log 2>&1 &
$(get_docker_compose_command "build") build > install_compose_build_output.log 2>&1 &
BUILD_PID=$!
while kill -0 $BUILD_PID 2>/dev/null; do
printf "."
@@ -511,12 +539,12 @@ handle_build() {
printf "${CYAN}> Starting Docker Compose stack...${NC}\n"
if [ "$VERBOSE" = true ]; then
docker compose -f docker-compose.yml -f docker-compose.build.yml up -d || {
$(get_docker_compose_command "build") up -d || {
printf "${RED}> Failed to start Docker Compose stack${NC}\n"
exit 1
}
else
docker compose -f docker-compose.yml -f docker-compose.build.yml up -d > /dev/null 2>&1 || {
$(get_docker_compose_command "build") up -d > /dev/null 2>&1 || {
printf "${RED}> Failed to start Docker Compose stack${NC}\n"
exit 1
}
@@ -535,8 +563,7 @@ handle_uninstall() {
# Check if -y flag was passed
if [ "$FORCE_YES" != "true" ]; then
# Ask for confirmation before proceeding
read -p "Are you sure you want to uninstall AliasVault? This will remove all containers and images. [y/N] " -n 1 -r
echo
read -p "Are you sure you want to uninstall AliasVault? This will remove all containers and images. [y/N]: " REPLY
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
printf "${YELLOW}> Uninstall cancelled.${NC}\n"
exit 0
@@ -601,4 +628,181 @@ handle_uninstall() {
printf "${MAGENTA}=========================================================${NC}\n"
}
# Function to handle SSL configuration
handle_ssl_configuration() {
printf "${YELLOW}+++ SSL Certificate Configuration +++${NC}\n"
printf "\n"
# Check if AliasVault is installed
if [ ! -f "docker-compose.yml" ]; then
printf "${RED}Error: AliasVault must be installed first.${NC}\n"
exit 1
fi
# Get the current hostname and SSL config from .env
CURRENT_HOSTNAME=$(grep "^HOSTNAME=" "$ENV_FILE" | cut -d '=' -f2)
LETSENCRYPT_ENABLED=$(grep "^LETSENCRYPT_ENABLED=" "$ENV_FILE" | cut -d '=' -f2)
printf "${CYAN}About SSL Certificates:${NC}\n"
printf "A default installation of AliasVault comes with a self-signed SSL certificate.\n"
printf "While self-signed certificates provide encryption, they will show security warnings in browsers.\n"
printf "\n"
printf "AliasVault also supports generating valid SSL certificates via Let's Encrypt.\n"
printf "Let's Encrypt certificates are trusted by browsers and will not show security warnings.\n"
printf "However, Let's Encrypt requires that:\n"
printf " - AliasVault is reachable from the internet via port 80/443\n"
printf " - You have configured a valid domain name (not localhost)\n"
printf "\n"
printf "Let's Encrypt certificates will be automatically renewed before expiry.\n"
printf "\n"
printf "${CYAN}Current Configuration:${NC}\n"
if [ "$LETSENCRYPT_ENABLED" = "true" ]; then
printf "Currently using: ${GREEN}Let's Encrypt certificates${NC}\n"
else
printf "Currently using: ${YELLOW}Self-signed certificates${NC}\n"
fi
printf "Current hostname: ${CYAN}${CURRENT_HOSTNAME}${NC}\n"
printf "\n"
printf "SSL Options:\n"
printf "1) Activate and/or request new Let's Encrypt certificate (recommended for production)\n"
printf "2) Activate and/or generate new self-signed certificate\n"
printf "3) Cancel\n"
printf "\n"
read -p "Select an option [1-3]: " ssl_option
case $ssl_option in
1)
configure_letsencrypt
;;
2)
generate_self_signed_cert
;;
3)
printf "${YELLOW}SSL configuration cancelled.${NC}\n"
exit 0
;;
*)
printf "${RED}Invalid option selected.${NC}\n"
exit 1
;;
esac
}
# Function to configure Let's Encrypt
configure_letsencrypt() {
printf "${CYAN}> Configuring Let's Encrypt SSL certificate...${NC}\n"
# Check if hostname is localhost
if [ "$CURRENT_HOSTNAME" = "localhost" ]; then
printf "${RED}Error: Let's Encrypt certificates cannot be issued for 'localhost'.${NC}\n"
printf "${YELLOW}Please configure a valid publically resolvable domain name (e.g. mydomain.com) before setting up Let's Encrypt.${NC}\n"
exit 1
fi
# Check if hostname is a valid domain
if ! [[ "$CURRENT_HOSTNAME" =~ \.[a-zA-Z]{2,}$ ]]; then
printf "${RED}Error: Invalid hostname '${CURRENT_HOSTNAME}'.${NC}\n"
printf "${YELLOW}Please configure a valid publically resolvable domain name (e.g. mydomain.com) before setting up Let's Encrypt.${NC}\n"
exit 1
fi
# Verify DNS is properly configured
printf "\n${YELLOW}Important: Before proceeding, ensure that:${NC}\n"
printf "1. AliasVault is currently running and accessible at ${CYAN}https://${CURRENT_HOSTNAME}${NC}\n"
printf "2. Your domain (${CYAN}${CURRENT_HOSTNAME}${NC}) is externally resolvable to this server's IP address\n"
printf "3. Ports 80 and 443 are open and accessible from the internet\n"
printf "\n"
read -p "Have you completed these steps? [y/N]: " REPLY
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
printf "${YELLOW}> Let's Encrypt configuration cancelled.${NC}\n"
exit 0
fi
# Get contact email for Let's Encrypt
SUPPORT_EMAIL=$(grep "^SUPPORT_EMAIL=" "$ENV_FILE" | cut -d '=' -f2)
LETSENCRYPT_EMAIL=""
while true; do
printf "\nPlease enter a valid email address that will be used for Let's Encrypt certificate notifications:\n"
read -p "Email: " LETSENCRYPT_EMAIL
if [[ "$LETSENCRYPT_EMAIL" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
printf "Confirm using ${CYAN}${LETSENCRYPT_EMAIL}${NC} for Let's Encrypt notifications? [y/N] "
read REPLY
if [[ $REPLY =~ ^[Yy]$ ]]; then
break
fi
else
printf "${RED}Invalid email format. Please try again.${NC}\n"
fi
done
# Create certbot directories
printf "${CYAN}> Creating Let's Encrypt directories...${NC}\n"
mkdir -p ./certificates/letsencrypt/www
# Request certificate using a temporary certbot container
printf "${CYAN}> Requesting Let's Encrypt certificate...${NC}\n"
docker run --rm \
--network aliasvault_default \
-v ./certificates/letsencrypt:/etc/letsencrypt:rw \
-v ./certificates/letsencrypt/www:/var/www/certbot:rw \
certbot/certbot certonly \
--webroot \
--webroot-path=/var/www/certbot \
--email "$LETSENCRYPT_EMAIL" \
--agree-tos \
--no-eff-email \
--non-interactive \
--domains ${CURRENT_HOSTNAME} \
--force-renewal
if [ $? -ne 0 ]; then
printf "${RED}Failed to obtain Let's Encrypt certificate.${NC}\n"
exit 1
fi
# Fix permissions on Let's Encrypt directories and files
sudo chmod -R 755 ./certificates/letsencrypt
# Ensure private keys remain secure
sudo find ./certificates/letsencrypt -type f -name "privkey*.pem" -exec chmod 600 {} \;
sudo find ./certificates/letsencrypt -type f -name "fullchain*.pem" -exec chmod 644 {} \;
# Update .env to indicate Let's Encrypt is enabled
update_env_var "LETSENCRYPT_ENABLED" "true"
# Restart only the reverse proxy with new configuration so it loads the new certificate
printf "${CYAN}> Restarting reverse proxy with Let's Encrypt configuration...${NC}\n"
$(get_docker_compose_command) up -d reverse-proxy
printf "${GREEN}> Let's Encrypt SSL certificate has been configured successfully!${NC}\n"
}
# Function to generate self-signed certificate
generate_self_signed_cert() {
printf "${CYAN}> Generating new self-signed certificate...${NC}\n"
# Disable Let's Encrypt
update_env_var "LETSENCRYPT_ENABLED" "false"
# Stop existing containers
printf "${CYAN}> Stopping existing containers...${NC}\n"
docker compose down
# Remove existing certificates
rm -f ./certificates/ssl/cert.pem ./certificates/ssl/key.pem
# Remove Let's Encrypt directories
rm -rf ./certificates/letsencrypt
# Start containers (which will generate new self-signed certs)
printf "${CYAN}> Restarting services...${NC}\n"
docker compose up -d
printf "${GREEN}> New self-signed certificate has been generated successfully!${NC}\n"
}
main "$@"

View File

@@ -39,24 +39,30 @@ http {
server {
listen 80;
server_name _;
return 301 https://$host$request_uri;
# Handle ACME challenge for Let's Encrypt certificate validation
location /.well-known/acme-challenge/ {
allow all;
root /var/www/certbot;
try_files $uri =404;
default_type "text/plain";
add_header Cache-Control "no-cache";
break;
}
# Redirect all other HTTP traffic to HTTPS
location / {
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;
}
# Include the appropriate SSL certificate configuration generated
# by the entrypoint script.
include /etc/nginx/ssl.conf;
# Admin interface
location /admin {
@@ -81,5 +87,14 @@ http {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# 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;
}
}
}