481 lines
15 KiB
Bash
481 lines
15 KiB
Bash
#!/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 "$@"
|
||
|