Files
selfhost/init-mail.sh
2025-12-06 09:47:55 -08:00

481 lines
15 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
#set -euo pipefail # Exit on error, unset variables are errors
# ---------------------------
# Configuration
# ---------------------------
# Mail
MAIL_CONFIG_DIR="/mnt/Nextcloud/mail/config"
ACCOUNT_FILE="$MAIL_CONFIG_DIR/postfix-accounts.cf"
ADMIN_EMAIL="admin@poppyglen.cc"
# Caddy WAF
CADDY_WAF_DIR="./caddy/waf"
# Sunshine
SUNSHINE_CONFIG_DIR="/mnt/Nextcloud/sunshine/config"
SUNSHINE_CONFIG_FILE="$SUNSHINE_CONFIG_DIR/sunshine.conf"
SUNSHINE_USERNAME="${SUNSHINE_USERNAME:-sunshine_admin}"
SUNSHINE_PASSWORD="${SUNSHINE_PASSWORD:-}"
# Authelia
AUTHELIA_CONFIG_DIR="./authelia"
AUTHELIA_USERS_FILE="$AUTHELIA_CONFIG_DIR/users_database.yml"
AUTHELIA_CONFIG_FILE="$AUTHELIA_CONFIG_DIR/configuration.yml"
AUTHELIA_REDIS_DIR="/mnt/Nextcloud/authelia-redis"
# MinIO
MINIO_DATA_DIR="/mnt/Nextcloud/minio-data"
# ---------------------------
# General Functions
# ---------------------------
require_root() {
if [[ "$(id -u)" -ne 0 ]]; then
echo "❌ Error: This script must be run as root or with sudo." >&2
exit 1
fi
}
load_env() {
if [[ -f ".env" ]]; then
echo "📄 Loading environment variables from .env..."
while IFS='=' read -r key value; do
[[ -z "$key" || "$key" =~ ^# || "$key" =~ \[|\{ ]] && continue
export "$key=$value"
done < .env
fi
}
# ---------------------------
# Mail Setup Functions
# ---------------------------
get_password() {
if [[ -z "${MAIL_ADMIN_PASSWORD:-}" ]]; then
read -sp "Enter a password for the mail admin ($ADMIN_EMAIL): " MAIL_ADMIN_PASSWORD
echo
else
echo "✅ Loaded MAIL_ADMIN_PASSWORD from .env"
fi
if [[ -z "$MAIL_ADMIN_PASSWORD" ]]; then
echo "❌ Error: Password cannot be empty." >&2
exit 1
fi
}
generate_hash() {
local password="$1"
docker run --rm docker.io/mailserver/docker-mailserver:latest \
doveadm pw -s SHA512-CRYPT -p "$password"
}
create_account_file() {
echo "📝 Creating account file: $ACCOUNT_FILE"
mkdir -p "$MAIL_CONFIG_DIR"
echo "$ADMIN_EMAIL|$HASH" > "$ACCOUNT_FILE"
# Add additional users from USERS_JSON
if [[ -n "${USERS_JSON:-}" ]]; then
echo "📋 Adding additional users from USERS_JSON..."
echo "$USERS_JSON" | jq -c '.[]' | while read -r user; do
username=$(echo "$user" | jq -r '.username')
password=$(echo "$user" | jq -r '.password')
if [[ -z "$username" || -z "$password" ]]; then
echo "⚠️ Skipping invalid user entry: $user"
continue
fi
user_hash=$(generate_hash "$password")
echo "$username@$MAIL_DOMAIN|$user_hash" >> "$ACCOUNT_FILE"
echo " Added user: $username@$MAIL_DOMAIN"
done
fi
}
setup_mail() {
echo "📧 Mail Server First-Time Setup..."
if [[ -f "$ACCOUNT_FILE" ]]; then
read -p "⚠️ A mail account file already exists. Overwrite? (y/N) " -r response
if [[ ! "$response" =~ ^[Yy]$ ]]; then
echo "Skipping mail setup."
return
fi
fi
get_password
HASH=$(generate_hash "$MAIL_ADMIN_PASSWORD")
create_account_file
echo "✅ Mail setup complete."
}
# ---------------------------
# Sunshine Config Setup
# ---------------------------
setup_sunshine_config() {
echo "☀️ Setting up Sunshine config..."
mkdir -p "$SUNSHINE_CONFIG_DIR"
if [[ -f "$SUNSHINE_CONFIG_FILE" ]]; then
read -p "⚠️ Sunshine config already exists. Overwrite? (y/N) " -r response
if [[ ! "$response" =~ ^[Yy]$ ]]; then
echo "Skipping Sunshine config setup."
return
fi
rm "$SUNSHINE_CONFIG_FILE"
fi
local password="$SUNSHINE_PASSWORD"
if [[ -z "$password" ]]; then
read -sp "Enter a password for Sunshine user ($SUNSHINE_USERNAME): " password
echo
fi
if [[ -z "$password" ]]; then
echo "❌ Error: Sunshine password cannot be empty." >&2
exit 1
fi
echo "🔐 Creating Sunshine credentials..."
docker compose run --rm sunshine --creds "$SUNSHINE_USERNAME" "$password"
sed -i '1i username = "'"$SUNSHINE_USERNAME"'"' "$SUNSHINE_CONFIG_FILE"
echo "🔧 Setting permissions for Sunshine config..."
chown -R 1000:1000 "$SUNSHINE_CONFIG_DIR"
echo "✅ Sunshine config created successfully!"
}
# ---------------------------
# Caddy WAF Setup
# ---------------------------
setup_caddy_waf() {
echo "🛡️ Setting up Caddy WAF..."
if ! command -v git &> /dev/null; then
echo "❌ Error: git is not installed. Please install git to continue." >&2
exit 1
fi
mkdir -p "$CADDY_WAF_DIR"
local owasp_crs_dir="$CADDY_WAF_DIR/owasp-crs"
if [[ -d "$owasp_crs_dir" ]]; then
echo "✅ OWASP Core Rule Set already exists. Skipping clone."
else
echo "⏳ Cloning OWASP Core Rule Set..."
git clone https://github.com/coreruleset/coreruleset.git "$owasp_crs_dir"
fi
local crs_setup_conf="$owasp_crs_dir/crs-setup.conf"
if [[ ! -f "$crs_setup_conf" ]]; then
echo "📝 Creating crs-setup.conf from example..."
cp "$owasp_crs_dir/crs-setup.conf.example" "$crs_setup_conf"
fi
local coraza_conf="$CADDY_WAF_DIR/coraza.conf"
if [[ -f "$coraza_conf" ]]; then
echo "✅ coraza.conf already exists. Skipping."
else
echo "📝 Creating main WAF configuration (coraza.conf)..."
cat <<EOF > "$coraza_conf"
# This file tells Coraza where to find the OWASP Core Rule Set.
Include @owasp_crs/crs-setup.conf
Include @owasp_crs/rules/*.conf
EOF
fi
echo "✅ Caddy WAF setup is complete!"
}
# ---------------------------
# Authelia Setup
# ---------------------------
generate_authelia_hash() {
local pass="$1"
local output
output=$(docker run --rm authelia/authelia authelia crypto hash generate argon2 --password "$pass" 2>&1)
echo "$output" | grep -o '\$argon2id\$.*'
}
setup_authelia() {
echo "🔐 Setting up Authelia..."
if ! command -v openssl &> /dev/null; then
echo "❌ Error: openssl is not installed. Please install it to generate secure secrets." >&2
exit 1
fi
mkdir -p "$AUTHELIA_CONFIG_DIR"
if [[ -f "$AUTHELIA_USERS_FILE" ]]; then
read -p "⚠️ Authelia user database already exists. Overwrite? (y/N) " -r response
if [[ ! "$response" =~ ^[Yy]$ ]]; then
echo "Skipping Authelia setup."
return
fi
fi
# --- Admin User Setup ---
local admin_username="${AUTHELIA_ADMIN_USER:-}"
local admin_password="${AUTHELIA_ADMIN_PASSWORD:-}"
if [[ -z "$admin_username" ]]; then
read -p "Enter a username for the Authelia admin: " admin_username
else
echo "✅ Loaded AUTHELIA_ADMIN_USER from .env"
fi
if [[ -z "$admin_password" ]]; then
read -sp "Enter a password for Authelia user '$admin_username': " admin_password
echo
else
echo "✅ Loaded AUTHELIA_ADMIN_PASSWORD from .env"
fi
if [[ -z "$admin_username" || -z "$admin_password" ]]; then
echo "❌ Error: Authelia admin username and password cannot be empty." >&2
exit 1
fi
echo "⏳ Generating password hash for admin user..."
local admin_hash
admin_hash=$(generate_authelia_hash "$admin_password")
if [[ -z "$admin_hash" ]]; then
echo "❌ Error: Could not generate a valid hash for the admin user." >&2
exit 1
fi
# --- Authelia Configuration File Generation ---
echo "⏳ Generating secrets..."
local session_secret
session_secret=$(openssl rand -hex 32)
local encryption_key
encryption_key=$(openssl rand -hex 32)
local jwt_secret
jwt_secret=$(openssl rand -hex 32)
echo "📝 Creating Authelia configuration file..."
cat <<EOF > "$AUTHELIA_CONFIG_FILE"
# Main server configuration
server:
address: tcp://0.0.0.0:9091
# Session configuration using modern format
session:
secret: "$session_secret"
cookies:
- name: poppy_session
domain: poppyglen.cc
authelia_url: https://auth.poppyglen.cc
# Use Redis for session storage for better performance
redis:
host: authelia-redis
port: 6379
# Storage for user data, password reset tokens, etc.
storage:
encryption_key: "$encryption_key"
# Use a local SQLite database for persistent storage
local:
path: /config/db.sqlite3
# User authentication backend
authentication_backend:
file:
path: /config/users_database.yml
# Access control rules
access_control:
default_policy: deny
rules:
- domain: "cloud.poppyglen.cc"
policy: bypass
resources:
- '^/index\.php/apps/memories/static/go-vod\?arch=.+$'
methods: [GET]
- domain: "cloud.poppyglen.cc"
policy: bypass
resources:
- '^/apps/circles/async/.*$'
methods: [GET]
- domain: "cloud.poppyglen.cc"
policy: bypass
resources:
- '^/index\.php/apps/richdocuments/wopi/.*$'
- domain: "cloud.poppyglen.cc"
policy: bypass
resources:
- '^/\.well-known/(card|cal)dav$'
- domain: "cloud.poppyglen.cc"
policy: bypass
resources:
- '^/remote\.php/(dav|webdav)/.*$'
- domain: "cloud.poppyglen.cc"
policy: bypass
resources:
- '^/ocs/v[12]\.php/.*$'
- '^/remote\.php/status\.php$'
- domain:
- "cloud.poppyglen.cc"
- "immich.poppyglen.cc"
- "jellyfin.poppyglen.cc"
policy: two_factor
subject:
- "group:admins"
- "group:users"
- "group:family"
- domain: "auth.poppyglen.cc"
policy: bypass
# Required for password reset functionality
identity_validation:
reset_password:
jwt_secret: "$jwt_secret"
# Notification settings
notifier:
disable_startup_check: true
smtp:
address: "submission://mail:587"
username: $MAIL_ADMIN_EMAIL
password: $MAIL_ADMIN_PASSWORD
sender: "Authelia <admin@poppyglen.cc>"
identifier: "authelia.poppyglen.cc"
tls:
skip_verify: true
# disable_require_tls: false
# filesystem:
# filename: /config/notification.log
EOF
# --- Authelia User Database File Generation ---
echo "📝 Creating Authelia user database for admin..."
{
echo "users:"
echo " $admin_username:"
echo " displayname: \"Admin User\""
echo " password: '$admin_hash'"
echo " email: \"admin@poppyglen.cc\""
echo " groups:"
echo " - admins"
} > "$AUTHELIA_USERS_FILE"
# --- START: New section to add users from JSON ---
if [[ -n "${USERS_JSON:-}" ]]; then
echo "📋 Adding additional Authelia users from USERS_JSON..."
echo "$USERS_JSON" | jq -c '.[]' | while read -r user_json; do
local username=$(echo "$user_json" | jq -r '.username')
local password=$(echo "$user_json" | jq -r '.password')
local name=$(echo "$user_json" | jq -r '.name')
local email=$(echo "$user_json" | jq -r '.email')
if [[ -z "$username" || -z "$password" ]]; then
echo "⚠️ Skipping invalid user entry: $user_json"
continue
fi
echo "⏳ Generating password hash for user '$username'..."
local user_hash
user_hash=$(generate_authelia_hash "$password")
if [[ -z "$user_hash" ]]; then
echo "❌ Error: Could not generate a valid hash for user '$username'." >&2
continue
fi
# Append the new user to the users_database.yml file
{
echo " $username:"
echo " displayname: \"$name\""
echo " password: '$user_hash'"
echo " email: \"$email\""
echo " groups:"
echo " - users"
echo " - family"
} >> "$AUTHELIA_USERS_FILE"
echo " Added Authelia user: $username"
done
fi
# --- END: New section ---
echo "✅ Authelia setup is complete!"
}
# ---------------------------
# Cleanup Function
# ---------------------------
clean_services() {
local COMPOSE_FILE="docker-compose.yml"
if [[ ! -f "$COMPOSE_FILE" ]]; then
echo "❌ Error: $COMPOSE_FILE not found in the current directory." >&2
exit 1
fi
echo "This script will PERMANENTLY DELETE data for the selected services."
read -p "Are you sure you want to proceed with cleanup? (y/N) " -r response
if [[ ! "$response" =~ ^[Yy]$ ]]; then
echo "Operation cancelled."
exit 0
fi
echo "⬇️ Bringing down Docker containers and networks..."
docker compose down --remove-orphans
# Helper function to clean directories
_clean_dir_contents() {
local service_name="$1"
shift
local dirs_to_clean=("$@")
read -p "Do you want to delete ALL data for $service_name? (y/N) " -r clean_response
if [[ "$clean_response" =~ ^[Yy]$ ]]; then
echo "🗑️ Cleaning $service_name directories..."
for vol in "${dirs_to_clean[@]}"; do
if [[ -d "$vol" ]]; then
echo "Cleaning host directory: $vol"
find "$vol" -mindepth 1 -delete
else
echo " -> Directory not found, skipping."
fi
done
echo "$service_name data has been cleared."
else
echo "Skipping $service_name data cleanup."
fi
}
_clean_dir_contents "Authelia" "$AUTHELIA_CONFIG_DIR" "$AUTHELIA_REDIS_DIR"
_clean_dir_contents "Nextcloud Core" "/mnt/Nextcloud/Nextcloud" "/mnt/Nextcloud/nextcloud_db" "/mnt/Nextcloud/redis" "/mnt/Nextcloud/clamav_data" "/mnt/Nextcloud/es_data"
_clean_dir_contents "MinIO S3 Storage" "$MINIO_DATA_DIR"
_clean_dir_contents "Immich" "/mnt/Nextcloud/immich/immich_db" "/mnt/Nextcloud/immich/immich_upload" "/mnt/Nextcloud/immich/immich_models"
_clean_dir_contents "Jellyfin" "/mnt/Nextcloud/jellyfin/config" "/mnt/Nextcloud/jellyfin/cache"
_clean_dir_contents "Mail Server" "/mnt/Nextcloud/mail/maildata" "/mnt/Nextcloud/mail/mailstate" "/mnt/Nextcloud/mail/mail-logs" "/mnt/Nextcloud/mail/config"
_clean_dir_contents "Sunshine" "/mnt/Nextcloud/sunshine"
_clean_dir_contents "Technitium DNS" "/mnt/Nextcloud/technitium/config"
_clean_dir_contents "Ollama" "/mnt/Nextcloud/ollama"
_clean_dir_contents "Caddy (Redis Data)" "/mnt/Nextcloud/caddy_redis"
echo "✅ Cleanup complete."
}
# ---------------------------
# Main Execution
# ---------------------------
main() {
require_root
if [[ "${1:-}" == "clean" ]]; then
clean_services
else
echo "🚀 Services First-Time Setup Script"
echo "------------------------------------"
load_env
# --- Run Setup Functions ---
setup_mail
setup_caddy_waf
setup_authelia
setup_sunshine_config
echo "------------------------------------"
echo "✅ All setups complete! You can now run: docker compose up -d"
echo "To clean up this setup, run: sudo ./init-services.sh clean"
fi
}
main "$@"