#!/bin/bash set -e # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color log() { echo -e "${BLUE}➜${NC} $1"; } success() { echo -e "${GREEN}✓${NC} $1"; } warn() { echo -e "${YELLOW}⚠${NC} $1"; } error() { echo -e "${RED}✗${NC} $1"; exit 1; } echo "=======================================" echo " Organization Stack Deployment" echo "=======================================" echo #============================================================================= # 1. Initialize .env if missing #============================================================================= if [ ! -f .env ]; then log "Creating .env from template..." cp .env.example .env success ".env created" warn "Please edit .env with your configuration, then run deploy.sh again" exit 0 else log ".env exists, checking configuration..." fi # Source the .env file set -a source .env set +a # Validate required variables if [ -z "$BASE_DOMAIN" ] || [ -z "$REMOTE_USER" ] || [ -z "$REMOTE_HOST" ]; then error "Required variables missing in .env: BASE_DOMAIN, REMOTE_USER, REMOTE_HOST" fi # Handle REMOTE_PATH (supports both absolute and relative paths) if [ -z "$REMOTE_PATH" ]; then # Backwards compatibility: use old REMOTE_DIR if REMOTE_PATH not set REMOTE_PATH="${REMOTE_DIR:-org-stack}" fi # Determine if path is absolute or relative if [[ "$REMOTE_PATH" = /* ]]; then DEPLOY_PATH="$REMOTE_PATH" log "Deploying to absolute path: $DEPLOY_PATH" else DEPLOY_PATH="~/$REMOTE_PATH" log "Deploying to path relative to home: $DEPLOY_PATH" fi success "Configuration validated" #============================================================================= # 2. Sync files to remote server #============================================================================= log "Syncing files to remote server ${REMOTE_USER}@${REMOTE_HOST}..." # Create remote directory with proper permissions ssh -p ${REMOTE_PORT:-22} ${REMOTE_USER}@${REMOTE_HOST} "mkdir -p $DEPLOY_PATH" || \ error "Failed to create remote directory" # Sync all necessary files (excluding secrets - they'll be generated remotely) rsync -avz --delete \ --exclude='secrets/' \ --exclude='.git/' \ --exclude='*.tar.gz' \ --exclude='backups/' \ --exclude='data/' \ ./ ${REMOTE_USER}@${REMOTE_HOST}:${DEPLOY_PATH}/ || \ error "Failed to sync files" success "Files synced to remote server" #============================================================================= # 3. Run remote deployment script #============================================================================= log "Running deployment on remote server..." # Export variables for remote script ssh -p ${REMOTE_PORT:-22} ${REMOTE_USER}@${REMOTE_HOST} \ "export DEPLOY_PATH='${DEPLOY_PATH}' ADMIN_GROUP='${ADMIN_GROUP}'; bash -s" <<'REMOTE_SCRIPT' set -e # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' log() { echo -e "${BLUE}➜${NC} $1"; } success() { echo -e "${GREEN}✓${NC} $1"; } error() { echo -e "${RED}✗${NC} $1"; exit 1; } # Change to deployment directory (expand tilde if present) eval cd "$DEPLOY_PATH" # Source environment set -a source .env set +a #============================================================================= # Generate secrets #============================================================================= log "Checking secrets..." mkdir -p secrets/lldap secrets/authelia # Function to generate random secret generate_secret_file() { local file_path=$1 local length=${2:-32} if [ ! -f "$file_path" ]; then openssl rand -hex $length > "$file_path" chmod 600 "$file_path" log "Generated: $file_path" fi } # Function to generate random password (alphanumeric + special chars) generate_password_file() { local file_path=$1 local length=${2:-32} if [ ! -f "$file_path" ]; then # Generate password with letters, numbers, and some special chars < /dev/urandom tr -dc 'A-Za-z0-9!@#%^&*' | head -c${length} > "$file_path" chmod 600 "$file_path" log "Generated: $file_path" fi } # Generate all secret files generate_secret_file "secrets/lldap/JWT_SECRET" 32 generate_password_file "secrets/lldap/LDAP_USER_PASS" 32 generate_secret_file "secrets/authelia/JWT_SECRET" 32 generate_secret_file "secrets/authelia/SESSION_SECRET" 32 generate_secret_file "secrets/authelia/STORAGE_ENCRYPTION_KEY" 32 generate_secret_file "secrets/authelia/OIDC_HMAC_SECRET" 32 generate_secret_file "secrets/authelia/RESET_PASSWORD_JWT_SECRET" 32 # Generate Gitea OIDC secret in .env if missing if ! grep -q "^GITEA_OIDC_CLIENT_SECRET=" .env || [ -z "$GITEA_OIDC_CLIENT_SECRET" ]; then GITEA_OIDC_CLIENT_SECRET=$(openssl rand -hex 36) echo "GITEA_OIDC_CLIENT_SECRET=${GITEA_OIDC_CLIENT_SECRET}" >> .env log "Generated GITEA_OIDC_CLIENT_SECRET" export GITEA_OIDC_CLIENT_SECRET fi # Generate RSA key using Docker if [ ! -f "secrets/authelia/OIDC_PRIVATE_KEY" ]; then log "Generating RSA private key using Docker..." TEMP_DIR=$(mktemp -d) docker run --rm --user "$(id -u):$(id -g)" -v "$TEMP_DIR:/keys" authelia/authelia:latest \ authelia crypto pair rsa generate --bits 2048 --directory /keys >/dev/null 2>&1 || \ error "Failed to generate RSA key" if [ -f "$TEMP_DIR/private.pem" ]; then mv "$TEMP_DIR/private.pem" "secrets/authelia/OIDC_PRIVATE_KEY" chmod 600 "secrets/authelia/OIDC_PRIVATE_KEY" rm -rf "$TEMP_DIR" log "Generated: OIDC_PRIVATE_KEY" else rm -rf "$TEMP_DIR" error "RSA key generation failed" fi fi success "All secrets ready" #============================================================================= # Set up permissions for multi-admin access #============================================================================= if [ -n "$ADMIN_GROUP" ]; then log "Configuring multi-admin permissions for group: $ADMIN_GROUP" # Verify group exists if ! getent group "$ADMIN_GROUP" >/dev/null 2>&1; then error "Group '$ADMIN_GROUP' does not exist. Create it with: sudo groupadd $ADMIN_GROUP" fi # Set group ownership on all files (preserve user owner) chgrp -R "$ADMIN_GROUP" . 2>/dev/null || \ error "Failed to set group ownership. Ensure user is in group: sudo usermod -aG $ADMIN_GROUP $USER" # Set permissions: # - Directories: 750 (rwxr-x---) - owner full, group read+execute, others none # - Regular files: 640 (rw-r-----) - owner read+write, group read, others none # - Secrets: 600 (rw-------) - owner only (Docker will read as owner) # - Executables: 750 (rwxr-x---) - owner execute, group execute find . -type d -exec chmod 750 {} \; find . -type f -exec chmod 640 {} \; find . -type f -name '*.sh' -exec chmod 750 {} \; chmod 750 manage.sh deploy.sh 2>/dev/null || true # Secrets should be more restrictive (owner only) if [ -d "secrets" ]; then chmod 750 secrets find secrets -type d -exec chmod 750 {} \; find secrets -type f -exec chmod 600 {} \; fi success "Permissions configured for multi-admin access" log "Group members can read configs and manage with: cd $DEPLOY_PATH && ./manage.sh" else log "Single-user mode (ADMIN_GROUP not set)" fi #============================================================================= # Derive LDAP_BASE_DN #============================================================================= if [ "$LDAP_BASE_DN" = "AUTO" ]; then log "Deriving LDAP_BASE_DN from BASE_DOMAIN..." DERIVED_DN=$(echo "$BASE_DOMAIN" | sed 's/\./,dc=/g' | sed 's/^/dc=/') sed -i "s|^LDAP_BASE_DN=.*|LDAP_BASE_DN=${DERIVED_DN}|" .env export LDAP_BASE_DN="$DERIVED_DN" success "LDAP_BASE_DN set to: $DERIVED_DN" fi #============================================================================= # Hash Gitea OIDC client secret #============================================================================= log "Hashing Gitea OIDC client secret..." # Re-source .env to get GITEA_OIDC_CLIENT_SECRET source .env GITEA_OIDC_CLIENT_SECRET_HASH=$(docker run --rm authelia/authelia:latest \ authelia crypto hash generate pbkdf2 --variant sha512 --password "$GITEA_OIDC_CLIENT_SECRET" 2>&1 | \ grep 'Digest:' | awk '{print $2}') if [ -z "$GITEA_OIDC_CLIENT_SECRET_HASH" ]; then error "Failed to hash Gitea client secret" fi export GITEA_OIDC_CLIENT_SECRET_HASH success "Gitea client secret hashed" #============================================================================= # Set authentication policy #============================================================================= if [ "$REQUIRE_2FA" = "true" ]; then export AUTH_POLICY="two_factor" else export AUTH_POLICY="one_factor" fi #============================================================================= # Generate Caddyfile from template #============================================================================= log "Generating Caddyfile..." if [ "$USE_SELF_SIGNED_CERTS" = "true" ]; then envsubst < Caddyfile.test.template > Caddyfile success "Generated Caddyfile (self-signed mode)" else envsubst < Caddyfile.production.template > Caddyfile success "Generated Caddyfile (Let's Encrypt mode)" fi #============================================================================= # Generate Authelia configuration #============================================================================= log "Generating Authelia configuration..." # Use appropriate template based on SMTP_ENABLED if [ "${SMTP_ENABLED:-false}" = "true" ]; then log "Using SMTP email notifications..." envsubst < authelia/configuration.yml.smtp.template > authelia/configuration.yml else log "Using filesystem notifier..." envsubst < authelia/configuration.yml.filesystem.template > authelia/configuration.yml fi success "Generated authelia/configuration.yml" #============================================================================= # Start services #============================================================================= log "Starting services..." docker compose up -d || error "Failed to start services" success "Services started successfully!" #============================================================================= # Display service information #============================================================================= echo echo "=======================================" echo " Deployment Complete!" echo "=======================================" echo echo "Services available at:" echo " • Gitea: https://${GITEA_SUBDOMAIN}.${BASE_DOMAIN}" echo " • Wiki: https://${WIKI_SUBDOMAIN}.${BASE_DOMAIN}" echo " • Authelia: https://${AUTHELIA_SUBDOMAIN}.${BASE_DOMAIN}" echo " • lldap: https://${LLDAP_SUBDOMAIN}.${BASE_DOMAIN}" echo " • Registration: https://${REGISTRATION_SUBDOMAIN}.${BASE_DOMAIN}" echo # Read and display credentials if [ -f "secrets/lldap/LDAP_USER_PASS" ]; then LLDAP_PASS=$(cat secrets/lldap/LDAP_USER_PASS) echo "lldap admin credentials:" echo " • Username: admin" echo " • Password: ${LLDAP_PASS}" echo fi if [ -n "$GITEA_OIDC_CLIENT_SECRET" ]; then echo "Gitea OIDC credentials:" echo " • Client ID: gitea" echo " • Client Secret: ${GITEA_OIDC_CLIENT_SECRET}" echo fi echo -e "${YELLOW}⚠${NC} Save these credentials securely!" echo REMOTE_SCRIPT if [ $? -eq 0 ]; then success "Deployment completed successfully on ${REMOTE_HOST}!" echo echo "Next steps:" echo " 1. Access lldap at https://${LLDAP_SUBDOMAIN}.${BASE_DOMAIN}" echo " 2. Create user accounts" echo " 3. Configure Gitea OIDC (see README.md)" echo else error "Deployment failed" fi