#!/usr/bin/env bash set -euo pipefail ############################################## # MailWolt # # Bootstrap Installer v1.0 # ############################################## # ===== Branding & Pfade (einmal ändern, überall wirksam) ===== APP_NAME="${APP_NAME:-MailWolt}" APP_ENV="production" APP_DEBUG="false" APP_USER="${APP_USER:-mailwolt}" APP_GROUP="${APP_GROUP:-www-data}" # Projektverzeichnis APP_DIR="/var/www/${APP_USER}" # Konfigbasis (statt /etc/falcomail -> /etc/${APP_USER}) CONF_BASE="/etc/${APP_USER}" CERT_DIR="${CONF_BASE}/ssl" CERT="${CERT_DIR}/cert.pem" KEY="${CERT_DIR}/key.pem" # Nginx vHost NGINX_SITE="/etc/nginx/sites-available/${APP_USER}.conf" NGINX_SITE_LINK="/etc/nginx/sites-enabled/${APP_USER}.conf" # DB DB_NAME="${DB_NAME:-${APP_USER}}" DB_USER="${DB_USER:-${APP_USER}}" # DB_PASS muss vorher generiert oder gesetzt werden # Git (Platzhalter; später ersetzen) GIT_REPO="${GIT_REPO:-https://git.nexlab.at/boban/mailwolt.git}" GIT_BRANCH="${GIT_BRANCH:-main}" # Node Setup (deb = apt Pakete; nodesource = NodeSource LTS) NODE_SETUP="${NODE_SETUP:-deb}" # ===== Styling ===== GREEN="\033[1;32m"; YELLOW="\033[1;33m"; RED="\033[1;31m"; CYAN="\033[1;36m"; GREY="\033[0;90m"; NC="\033[0m" BAR="──────────────────────────────────────────────────────────────────────────────" header() { echo -e "${CYAN}${BAR}${NC}" echo -e "${CYAN} 888b d888 d8b 888 888 888 888 888 ${NC}" echo -e "${CYAN} 8888b d8888 Y8P 888 888 o 888 888 888 ${NC}" echo -e "${CYAN} 88888b.d88888 888 888 d8b 888 888 888 ${NC}" echo -e "${CYAN} 888Y88888P888 8888b. 888 888 888 d888b 888 .d88b. 888 888888 ${NC}" echo -e "${CYAN} 888 Y888P 888 '88b 888 888 888d88888b888 d88''88b 888 888 ${NC}" echo -e "${CYAN} 888 Y8P 888 .d888888 888 888 88888P Y88888 888 888 888 888 ${NC}" echo -e "${CYAN} 888 ' 888 888 888 888 888 8888P Y8888 Y88..88P 888 Y88b. ${NC}" echo -e "${CYAN} 888 888 'Y888888 888 888 888P Y888 'Y88P' 888 'Y888 ${NC}" echo -e "${CYAN}${BAR}${NC}" echo } footer_ok() { local ip="$1" local app_name="${2:-$APP_NAME}" local app_dir="${3:-$APP_DIR}" local nginx_site="${4:-$NGINX_SITE}" local cert_dir="${5:-$CERT_DIR}" echo echo -e "${GREEN}${BAR}${NC}" echo -e "${GREEN} ✔ ${app_name} Bootstrap erfolgreich abgeschlossen${NC}" echo -e "${GREEN}${BAR}${NC}" echo -e " Aufruf: ${CYAN}http://${ip}${NC} ${GREY}| https://${ip}${NC}" echo -e " Laravel Root: ${GREY}${app_dir}${NC}" echo -e " Nginx Site: ${GREY}${nginx_site}${NC}" echo -e " Self-signed Cert: ${GREY}${cert_dir}/{cert.pem,key.pem}${NC}" echo -e " Postfix/Dovecot Ports aktiv: ${GREY}25, 465, 587, 110, 995, 143, 993${NC}" echo -e " Rspamd/OpenDKIM: ${GREY}aktiv (DKIM-Keys später im Wizard)${NC}" echo -e " Monit (Watchdog): ${GREY}installiert, NICHT aktiviert${NC}" echo -e "${GREEN}${BAR}${NC}" echo } log() { echo -e "${GREEN}[+]${NC} $*"; } warn() { echo -e "${YELLOW}[!]${NC} $*"; } err() { echo -e "${RED}[x]${NC} $*"; } require_root() { [ "$(id -u)" -eq 0 ] || { err "Bitte als root ausführen."; exit 1; }; } # ===== IP ermitteln ===== detect_ip() { local ip ip="$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{for (i=1;i<=NF;i++) if ($i=="src") {print $(i+1); exit}}')" || true [[ -n "${ip:-}" ]] || ip="$(hostname -I 2>/dev/null | awk '{print $1}')" [[ -n "${ip:-}" ]] || { err "Konnte Server-IP nicht ermitteln."; exit 1; } echo "$ip" } # ===== Secrets ===== gen() { head -c 512 /dev/urandom | tr -dc 'A-Za-z0-9' | head -c "${1:-28}" || true; } pw() { gen 28; } short() { gen 16; } # ===== Argument-Parsing ===== while [[ $# -gt 0 ]]; do case "$1" in -dev) APP_ENV="local"; APP_DEBUG="true" ;; -stag|-staging) APP_ENV="staging"; APP_DEBUG="false" ;; esac shift done # ===== Start ===== require_root header SERVER_IP="$(detect_ip)" APP_PW="${APP_PW:-$(pw)}" MAIL_HOSTNAME="${MAIL_HOSTNAME:-"bootstrap.local"}" # Wizard setzt später FQDN TZ="${TZ:-""}" # leer; Wizard setzt final echo -e "${GREY}Server-IP erkannt: ${SERVER_IP}${NC}" [ -n "$TZ" ] && { ln -fs "/usr/share/zoneinfo/${TZ}" /etc/localtime || true; } log "Paketquellen aktualisieren…" export DEBIAN_FRONTEND=noninteractive apt-get update -y # ---- MariaDB-Workaround (fix für mariadb-common prompt) ---- log "MariaDB-Workaround vorbereiten…" mkdir -p /etc/mysql /etc/mysql/mariadb.conf.d [ -f /etc/mysql/mariadb.cnf ] || echo '!include /etc/mysql/mariadb.conf.d/*.cnf' > /etc/mysql/mariadb.cnf # ---- Basis-Pakete installieren ---- log "Pakete installieren… (dies kann einige Minuten dauern)" #apt-get install -y \ apt-get -y -o Dpkg::Options::="--force-confdef" \ -o Dpkg::Options::="--force-confold" install \ postfix postfix-mysql \ dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpd dovecot-mysql \ mariadb-server mariadb-client \ redis-server \ rspamd \ opendkim opendkim-tools \ nginx \ php php-fpm php-cli php-mbstring php-xml php-curl php-zip php-mysql php-redis php-gd php-sqlite3 unzip curl \ composer \ certbot python3-certbot-nginx \ fail2ban \ ca-certificates rsyslog sudo openssl netcat-openbsd monit git # ===== Verzeichnisse / User ===== log "Verzeichnisse und Benutzer anlegen…" mkdir -p ${CERT_DIR} /etc/postfix /etc/dovecot/conf.d /etc/rspamd/local.d /var/mail/vhosts id vmail >/dev/null 2>&1 || adduser --system --group --home /var/mail vmail chown -R vmail:vmail /var/mail id "$APP_USER" >/dev/null 2>&1 || adduser --disabled-password --gecos "" "$APP_USER" usermod -a -G www-data "$APP_USER" # ===== Self-signed TLS (SAN = IP) ===== CERT="${CERT_DIR}/cert.pem" KEY="${CERT_DIR}/key.pem" OSSL_CFG="${CERT_DIR}/openssl.cnf" if [ ! -s "$CERT" ] || [ ! -s "$KEY" ]; then log "Erzeuge Self-Signed TLS Zertifikat (SAN=IP:${SERVER_IP})…" cat > "$OSSL_CFG" < /etc/postfix/sql/mysql-virtual-mailbox-maps.cf < /etc/postfix/sql/mysql-virtual-alias-maps.cf < /etc/dovecot/dovecot.conf <<'CONF' !include_try /etc/dovecot/conf.d/*.conf CONF cat > /etc/dovecot/conf.d/10-mail.conf <<'CONF' protocols = imap pop3 lmtp mail_location = maildir:/var/mail/vhosts/%d/%n namespace inbox { inbox = yes } mail_privileged_group = mail CONF cat > /etc/dovecot/conf.d/10-auth.conf <<'CONF' disable_plaintext_auth = yes auth_mechanisms = plain login !include_try auth-sql.conf.ext CONF cat > /etc/dovecot/dovecot-sql.conf.ext < /etc/dovecot/conf.d/auth-sql.conf.ext <<'CONF' passdb { driver = sql args = /etc/dovecot/dovecot-sql.conf.ext } userdb { driver = static args = uid=vmail gid=vmail home=/var/mail/vhosts/%d/%n } CONF cat > /etc/dovecot/conf.d/10-master.conf <<'CONF' service lmtp { unix_listener /var/spool/postfix/private/dovecot-lmtp { mode = 0600 user = postfix group = postfix } } service auth { unix_listener /var/spool/postfix/private/auth { mode = 0660 user = postfix group = postfix } } service imap-login { inet_listener imap { port = 143 } inet_listener imaps { port = 993 ssl = yes } } service pop3-login { inet_listener pop3 { port = 110 } inet_listener pop3s { port = 995 ssl = yes } } CONF cat > /etc/dovecot/conf.d/10-ssl.conf < /etc/rspamd/local.d/worker-controller.inc <<'CONF' password = "admin"; bind_socket = "127.0.0.1:11334"; CONF systemctl enable --now rspamd || true cat > /etc/opendkim.conf <<'CONF' Syslog yes UMask 002 Mode sv Socket inet:8891@127.0.0.1 Canonicalization relaxed/simple On-BadSignature accept On-Default accept On-KeyNotFound accept On-NoSignature accept LogWhy yes OversignHeaders From # KeyTable / SigningTable später im Wizard CONF systemctl enable --now opendkim || true # ===== Redis ===== systemctl enable --now redis-server # ===== Nginx: Laravel vHost (80/443) ===== log "Nginx konfigurieren…" rm -f /etc/nginx/sites-enabled/default /etc/nginx/sites-available/default || true PHPV=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;') PHP_FPM_SOCK="/run/php/php${PHPV}-fpm.sock" [ -S "$PHP_FPM_SOCK" ] || PHP_FPM_SOCK="/run/php/php-fpm.sock" cat > ${NGINX_SITE} <> "${APP_DIR}/.env" # Bootstrap-Admin für den ersten Login BOOTSTRAP_USER="${APP_USER}" BOOTSTRAP_EMAIL="${APP_USER}@localhost" BOOTSTRAP_PASS="$(openssl rand -base64 18 | LC_ALL=C tr -dc 'A-Za-z0-9' | head -c 12)" BOOTSTRAP_HASH="$(php -r 'echo password_hash($argv[1], PASSWORD_BCRYPT);' "$BOOTSTRAP_PASS")" grep -q '^SETUP_PHASE=' "${APP_DIR}/.env" \ || echo "SETUP_PHASE=bootstrap" >> "${APP_DIR}/.env" sed -i "s|^SETUP_PHASE=.*|SETUP_PHASE=bootstrap|g" "${APP_DIR}/.env" grep -q '^BOOTSTRAP_ADMIN_USER=' "${APP_DIR}/.env" \ || echo "BOOTSTRAP_ADMIN_USER=${BOOTSTRAP_USER}" >> "${APP_DIR}/.env" sed -i "s|^BOOTSTRAP_ADMIN_USER=.*|BOOTSTRAP_ADMIN_USER=${BOOTSTRAP_USER}|g" "${APP_DIR}/.env" grep -q '^BOOTSTRAP_ADMIN_EMAIL=' "${APP_DIR}/.env" \ || echo "BOOTSTRAP_ADMIN_EMAIL=${BOOTSTRAP_EMAIL}" >> "${APP_DIR}/.env" sed -i "s|^BOOTSTRAP_ADMIN_EMAIL=.*|BOOTSTRAP_ADMIN_EMAIL=${BOOTSTRAP_EMAIL}|g" "${APP_DIR}/.env" grep -q '^BOOTSTRAP_ADMIN_PASSWORD_HASH=' "${APP_DIR}/.env" \ || echo "BOOTSTRAP_ADMIN_PASSWORD_HASH=${BOOTSTRAP_HASH}" >> "${APP_DIR}/.env" sed -i "s|^BOOTSTRAP_ADMIN_PASSWORD_HASH=.*|BOOTSTRAP_ADMIN_PASSWORD_HASH=${BOOTSTRAP_HASH}|g" "${APP_DIR}/.env" # ===== Composer Dependencies ===== log "Composer install…" sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && COMPOSER_ALLOW_SUPERUSER=1 composer install --no-dev --optimize-autoloader --no-interaction" # ===== App-Key & Migrations ===== log "App-Key generieren und Datenbank migrieren…" sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan key:generate --force" sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan migrate --force" sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan storage:link --force || true" # ===== Frontend Build ===== if [ -f "${APP_DIR}/package.json" ]; then log "Frontend bauen…" sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm ci --no-audit --no-fund || npm install" sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm run build" fi # ===== App-User/Gruppen & Rechte (am ENDE ausführen) ===== # User anlegen (nur falls noch nicht vorhanden) + Passwort setzen + Gruppe if ! id -u "$APP_USER" >/dev/null 2>&1; then adduser --disabled-password --gecos "" "$APP_USER" fi echo "${APP_USER}:${APP_PW}" | chpasswd usermod -a -G "$APP_GROUP" "$APP_USER" # Besitz & Rechte chown -R "$APP_USER":"$APP_GROUP" "$APP_DIR" find "$APP_DIR" -type d -exec chmod 775 {} \; find "$APP_DIR" -type f -exec chmod 664 {} \; chmod -R 775 "$APP_DIR"/storage "$APP_DIR"/bootstrap/cache if command -v setfacl >/dev/null 2>&1; then setfacl -R -m u:${APP_USER}:rwX,g:${APP_GROUP}:rwX \ "${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache" || true setfacl -dR -m u:${APP_USER}:rwX,g:${APP_GROUP}:rwX \ "${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache" || true fi grep -q 'umask 002' /home/${APP_USER}/.profile 2>/dev/null || echo 'umask 002' >> /home/${APP_USER}/.profile grep -q 'umask 002' /home/${APP_USER}/.bashrc 2>/dev/null || echo 'umask 002' >> /home/${APP_USER}/.bashrc # 7) npm respektiert umask – zur Sicherheit direkt setzen (für APP_USER) sudo -u "$APP_USER" -H bash -lc "npm config set umask 0002" >/dev/null 2>&1 || true # PHP-FPM-Socket group-writable machen FPM_POOL="/etc/php/${PHPV}/fpm/pool.d/www.conf" if [ -f "$FPM_POOL" ]; then sed -i 's/^;*listen\.owner.*/listen.owner = www-data/' "$FPM_POOL" sed -i 's/^;*listen\.group.*/listen.group = www-data/' "$FPM_POOL" sed -i 's/^;*listen\.mode.*/listen.mode = 0660/' "$FPM_POOL" systemctl restart php${PHPV}-fpm || true fi # 9) Optional: deinem Shell-/IDE-User ebenfalls Schreibrechte geben IDE_USER="${SUDO_USER:-}" if [ -n "$IDE_USER" ] && id "$IDE_USER" >/dev/null 2>&1 && command -v setfacl >/dev/null 2>&1; then usermod -a -G "$APP_GROUP" "$IDE_USER" || true setfacl -R -m u:${IDE_USER}:rwX "$APP_DIR" setfacl -dR -m u:${IDE_USER}:rwX "$APP_DIR" echo -e "${YELLOW}[i]${NC} Benutzer '${IDE_USER}' wurde für Schreibzugriff freigeschaltet (ACL + Gruppe ${APP_GROUP})." fi # Webstack neu laden systemctl reload nginx || true systemctl restart php*-fpm || true # Hinweis zur neuen Gruppenzugehörigkeit echo -e "${YELLOW}[i]${NC} SHELL: Du kannst dich nun als Benutzer '${APP_USER}' mit dem Passwort '${APP_PW}' anmelden." echo -e "${YELLOW}[i]${NC} Hinweis: Nach dem ersten Login solltest du das Passwort mit 'passwd ${APP_USER}' ändern." echo -e "${YELLOW}[i]${NC} Damit die Gruppenrechte (${APP_GROUP}) aktiv werden, bitte einmal ab- und wieder anmelden." # ===== Monit (Watchdog) – installiert, aber NICHT aktiviert ===== log "Monit (Watchdog) installieren (deaktiviert)" cat > /etc/monit/monitrc <<'EOF' set daemon 60 set logfile syslog facility log_daemon # set mailserver + set alert werden im Wizard gesetzt check process postfix with pidfile /var/spool/postfix/pid/master.pid start program = "/bin/systemctl start postfix" stop program = "/bin/systemctl stop postfix" if failed port 25 protocol smtp then restart check process dovecot with pidfile /var/run/dovecot/master.pid start program = "/bin/systemctl start dovecot" stop program = "/bin/systemctl stop dovecot" if failed port 143 type tcp then restart if failed port 993 type tcp ssl then restart check process mariadb with pidfile /var/run/mysqld/mysqld.pid start program = "/bin/systemctl start mariadb" stop program = "/bin/systemctl stop mariadb" if failed port 3306 type tcp then restart check process redis with pidfile /run/redis/redis-server.pid start program = "/bin/systemctl start redis-server" stop program = "/bin/systemctl stop redis-server" if failed port 6379 type tcp then restart check process rspamd with pidfile /run/rspamd/rspamd.pid start program = "/bin/systemctl start rspamd" stop program = "/bin/systemctl stop rspamd" if failed port 11332 type tcp then restart check process opendkim with pidfile /run/opendkim/opendkim.pid start program = "/bin/systemctl start opendkim" stop program = "/bin/systemctl stop opendkim" if failed port 8891 type tcp then restart check process nginx with pidfile /run/nginx.pid start program = "/bin/systemctl start nginx" stop program = "/bin/systemctl stop nginx" if failed port 80 type tcp then restart if failed port 443 type tcp ssl then restart EOF chmod 600 /etc/monit/monitrc systemctl disable --now monit || true apt-mark hold monit >/dev/null 2>&1 || true # ===== Smoke-Test (alle Ports, mit Timeouts) ===== log "Smoke-Test (Ports & Banner):" set +e printf "[25] " && timeout 6s bash -lc 'printf "EHLO localhost\r\nQUIT\r\n" | nc -v -w 4 127.0.0.1 25 2>&1' || true printf "[465] " && timeout 6s openssl s_client -connect 127.0.0.1:465 -brief -quiet &1' || true printf "[995] " && timeout 6s openssl s_client -connect 127.0.0.1:995 -brief -quiet &1' || true printf "[993] " && timeout 6s openssl s_client -connect 127.0.0.1:993 -brief -quiet