mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-09 15:56:11 -04:00
261 lines
9.5 KiB
Bash
261 lines
9.5 KiB
Bash
#!/bin/sh
|
|
|
|
# Create SSL directory if it doesn't exist
|
|
mkdir -p /etc/nginx/ssl
|
|
|
|
# Select the appropriate nginx configuration based on FORCE_HTTPS_REDIRECT
|
|
# Default to true (nginx-443.conf) for backward compatibility
|
|
# Only disable redirect if explicitly set to "false"
|
|
if [ "${FORCE_HTTPS_REDIRECT:-true}" = "false" ]; then
|
|
echo "Using nginx-80-443.conf (HTTP and HTTPS without redirect)"
|
|
cp /etc/nginx/nginx-80-443.conf /etc/nginx/nginx.conf
|
|
else
|
|
echo "Using nginx-443.conf (HTTP to HTTPS redirect enabled - default)"
|
|
cp /etc/nginx/nginx-443.conf /etc/nginx/nginx.conf
|
|
fi
|
|
|
|
# Substitute MAX_UPLOAD_SIZE placeholder in the active nginx config.
|
|
# Default to 100MB when unset (suitable for all-in-one images without explicit config).
|
|
MAX_UPLOAD_SIZE_MB_VALUE="${MAX_UPLOAD_SIZE_MB:-100}"
|
|
sed -i "s|__MAX_UPLOAD_SIZE__|${MAX_UPLOAD_SIZE_MB_VALUE}|g" /etc/nginx/nginx.conf
|
|
|
|
# Build the geo-block rules for the /admin IP allowlist from ADMIN_IP_ALLOWLIST.
|
|
# Empty -> "default 0;" (everyone allowed). "private" -> default-deny with
|
|
# loopback + RFC1918 carved out. Anything else is treated as a comma-separated
|
|
# list of CIDRs/IPs (loopback is always allowed). Tokens that don't look like
|
|
# an IPv4/IPv6 address or CIDR are skipped with a warning so a typo can't
|
|
# crash-loop nginx; if no valid tokens remain we fail closed (deny everything
|
|
# except loopback). When the resulting $admin_block is 1, the /admin location
|
|
# quietly routes the request to the client app via @admin_fallback rather than
|
|
# emitting a 403/404.
|
|
is_valid_cidr() {
|
|
case "$1" in
|
|
*:*)
|
|
printf '%s' "$1" | grep -Eq '^[0-9a-fA-F:]+(/[0-9]{1,3})?$'
|
|
;;
|
|
*)
|
|
printf '%s' "$1" | grep -Eq '^([0-9]{1,3}\.){3}[0-9]{1,3}(/[0-9]{1,2})?$'
|
|
;;
|
|
esac
|
|
}
|
|
|
|
build_admin_ip_geo_rules() {
|
|
local raw="${ADMIN_IP_ALLOWLIST:-}"
|
|
case "$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')" in
|
|
"")
|
|
printf '%s' "default 0;"
|
|
;;
|
|
private)
|
|
printf '%s' "default 1; 127.0.0.0/8 0; 10.0.0.0/8 0; 172.16.0.0/12 0; 192.168.0.0/16 0;"
|
|
;;
|
|
*)
|
|
local out="default 1; 127.0.0.0/8 0;"
|
|
local valid=0
|
|
local IFS=','
|
|
for cidr in $raw; do
|
|
cidr=$(printf '%s' "$cidr" | tr -d '[:space:]')
|
|
[ -z "$cidr" ] && continue
|
|
if is_valid_cidr "$cidr"; then
|
|
out="$out $cidr 0;"
|
|
valid=$((valid + 1))
|
|
else
|
|
printf 'warning: ADMIN_IP_ALLOWLIST: ignoring invalid CIDR/IP %s\n' "$cidr" >&2
|
|
fi
|
|
done
|
|
if [ "$valid" -eq 0 ]; then
|
|
printf 'warning: ADMIN_IP_ALLOWLIST has no valid entries; /admin will fall through to the client app for all non-loopback traffic\n' >&2
|
|
fi
|
|
printf '%s' "$out"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
ADMIN_IP_GEO_RULES_VALUE=$(build_admin_ip_geo_rules)
|
|
sed -i "s|__ADMIN_IP_GEO_RULES__|${ADMIN_IP_GEO_RULES_VALUE}|g" /etc/nginx/nginx.conf
|
|
|
|
# Build the set_real_ip_from directives from TRUSTED_PROXIES. These tell nginx
|
|
# which upstream proxies it should accept the X-Forwarded-For header from when
|
|
# determining the real client IP. Empty -> RFC1918 (backwards-compatible
|
|
# default). "none" -> no directives (raw $remote_addr is always used). Otherwise
|
|
# treated as a comma-separated list of CIDRs/IPs; invalid tokens are skipped
|
|
# with a warning so a typo can't crash-loop nginx, and if no valid tokens
|
|
# remain we emit no directives (equivalent to "none").
|
|
build_trusted_proxies_directives() {
|
|
local raw="${TRUSTED_PROXIES:-}"
|
|
case "$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')" in
|
|
"")
|
|
printf '%s' "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;"
|
|
;;
|
|
none)
|
|
printf '%s' ""
|
|
;;
|
|
*)
|
|
local out=""
|
|
local valid=0
|
|
local IFS=','
|
|
for cidr in $raw; do
|
|
cidr=$(printf '%s' "$cidr" | tr -d '[:space:]')
|
|
[ -z "$cidr" ] && continue
|
|
if is_valid_cidr "$cidr"; then
|
|
if [ -z "$out" ]; then
|
|
out="set_real_ip_from $cidr;"
|
|
else
|
|
out="$out set_real_ip_from $cidr;"
|
|
fi
|
|
valid=$((valid + 1))
|
|
else
|
|
printf 'warning: TRUSTED_PROXIES: ignoring invalid CIDR/IP %s\n' "$cidr" >&2
|
|
fi
|
|
done
|
|
if [ "$valid" -eq 0 ]; then
|
|
printf 'warning: TRUSTED_PROXIES has no valid entries; X-Forwarded-For will be ignored from all upstreams\n' >&2
|
|
fi
|
|
printf '%s' "$out"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
TRUSTED_PROXIES_VALUE=$(build_trusted_proxies_directives)
|
|
sed -i "s|__TRUSTED_PROXIES__|${TRUSTED_PROXIES_VALUE}|g" /etc/nginx/nginx.conf
|
|
|
|
# Function to check if certificate needs regeneration
|
|
needs_cert_regeneration() {
|
|
# If cert doesn't exist, need to generate
|
|
if [ ! -f /etc/nginx/ssl/cert.pem ] || [ ! -f /etc/nginx/ssl/key.pem ]; then
|
|
return 0
|
|
fi
|
|
|
|
# Check if we have a hostname marker file
|
|
if [ -f /etc/nginx/ssl/.hostname_marker ]; then
|
|
STORED_HOSTNAME=$(cat /etc/nginx/ssl/.hostname_marker)
|
|
if [ "$STORED_HOSTNAME" != "${HOSTNAME:-localhost}" ]; then
|
|
echo "Hostname changed from '$STORED_HOSTNAME' to '${HOSTNAME:-localhost}', regenerating certificate..."
|
|
return 0
|
|
fi
|
|
else
|
|
# No marker file, regenerate to be safe
|
|
return 0
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
# Function to check if a string is an IP address (IPv4 or IPv6)
|
|
is_ip_address() {
|
|
local value="$1"
|
|
|
|
# Check for IPv4 pattern
|
|
if echo "$value" | grep -qE '^([0-9]{1,3}\.){3}[0-9]{1,3}$'; then
|
|
# Validate each octet is <= 255
|
|
local valid=1
|
|
# Use a simple approach to split the IP address
|
|
local o1=$(echo "$value" | cut -d. -f1)
|
|
local o2=$(echo "$value" | cut -d. -f2)
|
|
local o3=$(echo "$value" | cut -d. -f3)
|
|
local o4=$(echo "$value" | cut -d. -f4)
|
|
for octet in "$o1" "$o2" "$o3" "$o4"; do
|
|
if [ "$octet" -gt 255 ]; then
|
|
valid=0
|
|
break
|
|
fi
|
|
done
|
|
if [ "$valid" -eq 1 ]; then
|
|
return 0 # It's a valid IPv4
|
|
fi
|
|
fi
|
|
|
|
# Check for IPv6 pattern (simplified check)
|
|
if echo "$value" | grep -qE '^([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}$|^::1$|^::$'; then
|
|
return 0 # It's likely IPv6
|
|
fi
|
|
|
|
return 1 # Not an IP address
|
|
}
|
|
|
|
# Generate self-signed SSL certificate if not exists or hostname changed
|
|
if needs_cert_regeneration; then
|
|
echo "Generating new SSL certificate (10 years validity)..."
|
|
|
|
HOSTNAME_VALUE="${HOSTNAME:-localhost}"
|
|
|
|
if [ "$HOSTNAME_VALUE" = "localhost" ] || [ -z "$HOSTNAME_VALUE" ]; then
|
|
# Default localhost-only configuration
|
|
openssl req -x509 -nodes -days 3650 -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"
|
|
else
|
|
# Determine if the hostname is an IP address or a DNS name
|
|
if is_ip_address "$HOSTNAME_VALUE"; then
|
|
# It's an IP address - use IP: prefix in SAN
|
|
SAN_ENTRY="IP:${HOSTNAME_VALUE}"
|
|
echo "Detected IP address: ${HOSTNAME_VALUE}"
|
|
else
|
|
# It's a DNS name - use DNS: prefix in SAN
|
|
SAN_ENTRY="DNS:${HOSTNAME_VALUE}"
|
|
echo "Detected hostname: ${HOSTNAME_VALUE}"
|
|
fi
|
|
|
|
# Generate certificate with the appropriate SAN entry
|
|
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
|
|
-keyout /etc/nginx/ssl/key.pem \
|
|
-out /etc/nginx/ssl/cert.pem \
|
|
-subj "/C=US/ST=State/L=City/O=AliasVault/CN=${HOSTNAME_VALUE}" \
|
|
-addext "subjectAltName=${SAN_ENTRY},DNS:localhost,IP:127.0.0.1"
|
|
fi
|
|
|
|
# Set proper permissions
|
|
chmod 644 /etc/nginx/ssl/cert.pem
|
|
chmod 600 /etc/nginx/ssl/key.pem
|
|
|
|
# Store current hostname for change detection
|
|
echo "${HOSTNAME:-localhost}" > /etc/nginx/ssl/.hostname_marker
|
|
chmod 644 /etc/nginx/ssl/.hostname_marker
|
|
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 and check if it started successfully
|
|
nginx
|
|
if [ $? -ne 0 ]; then
|
|
echo "Failed to start nginx, exiting..."
|
|
exit 1
|
|
fi
|
|
|
|
# Start certificate watcher if Let's Encrypt is enabled
|
|
if [ "${LETSENCRYPT_ENABLED}" = "true" ]; then
|
|
echo "Starting certificate watcher for automatic nginx reload..."
|
|
|
|
# Watch for changes and reload nginx
|
|
while true; do
|
|
# Watch the entire Let's Encrypt live directory for any certificate changes
|
|
inotifywait -e modify,create,delete,move -r /etc/nginx/ssl-letsencrypt 2>/dev/null
|
|
|
|
# Wait a moment for all certificate files to be written
|
|
sleep 2
|
|
|
|
echo "Certificate change detected, reloading nginx..."
|
|
nginx -s reload 2>&1
|
|
done &
|
|
fi
|
|
|
|
# Keep the container running and monitor nginx
|
|
while true; do
|
|
if ! pgrep nginx > /dev/null; then
|
|
echo "Nginx is not running, exiting..."
|
|
exit 1
|
|
fi
|
|
sleep 10
|
|
done
|