commit 8cad8eceb81c9ae5ae5d73760b3d95d85d4ce393 Author: boksbc Date: Thu Oct 16 10:38:34 2025 +0200 Init Mailwolt installer diff --git a/config/nginx/site.conf.tmpl b/config/nginx/site.conf.tmpl new file mode 100644 index 0000000..934e7a0 --- /dev/null +++ b/config/nginx/site.conf.tmpl @@ -0,0 +1,17 @@ +# ===================== HTTP (Port 80) ===================== +server { + listen 80 default_server; + listen [::]:80 default_server; + server_name _; + + # ACME HTTP-01 + location ^~ /.well-known/acme-challenge/ { + root /var/www/letsencrypt; + allow all; + } + + ## __HTTP_BODY__ +} + +# ===================== HTTPS (Port 443) ==================== +## __SSL_SERVER_BLOCK__ \ No newline at end of file diff --git a/scripts/10-provision.sh b/scripts/10-provision.sh new file mode 100644 index 0000000..ec7bf8c --- /dev/null +++ b/scripts/10-provision.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail +source ./lib.sh + +log "Pakete installieren …" +export DEBIAN_FRONTEND=noninteractive +apt-get update -y +# Minimal aber vollständig +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 \ + unzip curl composer git certbot python3-certbot-nginx \ + ca-certificates rsyslog sudo openssl monit acl netcat-openbsd + +log "Systemuser/Dirs …" +id vmail >/dev/null 2>&1 || adduser --system --group --home /var/mail vmail +id "$APP_USER" >/dev/null 2>&1 || adduser --disabled-password --gecos "" "$APP_USER" +usermod -a -G "$APP_GROUP" "$APP_USER" || true +install -d -m 0755 -o root -g root /var/www +install -d -m 0775 -o "$APP_USER" -g "$APP_GROUP" "$APP_DIR" + +log "MariaDB include-fix …" +mkdir -p /etc/mysql/mariadb.conf.d +[[ -f /etc/mysql/mariadb.cnf ]] || echo '!include /etc/mysql/mariadb.conf.d/*.cnf' > /etc/mysql/mariadb.cnf + +log "Redis absichern …" +REDIS_CONF="/etc/redis/redis.conf" +REDIS_PASS="${REDIS_PASS:-$(openssl rand -hex 16)}" +sed -i 's/^\s*#\?\s*bind .*/bind 127.0.0.1/' "$REDIS_CONF" +sed -i 's/^\s*#\?\s*protected-mode .*/protected-mode yes/' "$REDIS_CONF" +grep -qE '^\s*#?\s*requirepass ' "$REDIS_CONF" \ + && sed -i "s/^\s*#\?\s*requirepass .*/requirepass ${REDIS_PASS}/" "$REDIS_CONF" \ + || printf "\nrequirepass %s\n" "${REDIS_PASS}" >> "$REDIS_CONF" +systemctl enable --now redis-server +systemctl restart redis-server || true \ No newline at end of file diff --git a/scripts/20-ssl.sh b/scripts/20-ssl.sh new file mode 100644 index 0000000..6f6198e --- /dev/null +++ b/scripts/20-ssl.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +set -euo pipefail +source ./lib.sh + +CONF_BASE="/etc/${APP_USER}" +CERT_DIR="${CONF_BASE}/ssl" +UI_SSL_DIR="/etc/ssl/ui"; WEBMAIL_SSL_DIR="/etc/ssl/webmail"; MAIL_SSL_DIR="/etc/ssl/mail" +UI_CERT="${UI_SSL_DIR}/fullchain.pem"; UI_KEY="${UI_SSL_DIR}/privkey.pem" +WEBMAIL_CERT="${WEBMAIL_SSL_DIR}/fullchain.pem"; WEBMAIL_KEY="${WEBMAIL_SSL_DIR}/privkey.pem" +MAIL_CERT="${MAIL_SSL_DIR}/fullchain.pem"; MAIL_KEY="${MAIL_SSL_DIR}/privkey.pem" + +install -d -m 0750 "$CERT_DIR" +CERT="${CERT_DIR}/cert.pem"; KEY="${CERT_DIR}/key.pem" + +if [[ ! -s "$CERT" || ! -s "$KEY" ]]; then + log "Self-signed Zertifikat erzeugen …" + OSSL_CFG="${CERT_DIR}/openssl.cnf" + cat > "$OSSL_CFG" < /etc/postfix/sql/mysql-virtual-mailbox-maps.cf < /etc/postfix/sql/mysql-virtual-alias-maps.cf </dev/null 2>&1 || true \ No newline at end of file diff --git a/scripts/50-dovecot.sh b/scripts/50-dovecot.sh new file mode 100644 index 0000000..ba3eed2 --- /dev/null +++ b/scripts/50-dovecot.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +set -euo pipefail +source ./lib.sh + +MAIL_SSL_DIR="/etc/ssl/mail" +MAIL_CERT="${MAIL_SSL_DIR}/fullchain.pem" +MAIL_KEY="${MAIL_SSL_DIR}/privkey.pem" + +log "Dovecot konfigurieren …" + +# Hauptdatei +cat > /etc/dovecot/dovecot.conf <<'CONF' +!include_try /etc/dovecot/conf.d/*.conf +CONF + +# Mail-Location & Namespace +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 + +# Auth +cat > /etc/dovecot/conf.d/10-auth.conf <<'CONF' +disable_plaintext_auth = yes +auth_mechanisms = plain login +!include_try auth-sql.conf.ext +CONF + +# SQL-Anbindung +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 +chown root:dovecot /etc/dovecot/conf.d/auth-sql.conf.ext +chmod 640 /etc/dovecot/conf.d/auth-sql.conf.ext + +# Master-Services (LMTP + AUTH + Listener) +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 + +# SSL – stabile Mail-Pfade +DOVECOT_SSL_CONF="/etc/dovecot/conf.d/10-ssl.conf" +grep -q '^ssl\s*=' "$DOVECOT_SSL_CONF" 2>/dev/null || echo "ssl = required" >> "$DOVECOT_SSL_CONF" +if grep -q '^\s*ssl_cert\s*=' "$DOVECOT_SSL_CONF"; then + sed -i "s|^\s*ssl_cert\s*=.*|ssl_cert = <${MAIL_CERT}|" "$DOVECOT_SSL_CONF" +else + echo "ssl_cert = <${MAIL_CERT}" >> "$DOVECOT_SSL_CONF" +fi +if grep -q '^\s*ssl_key\s*=' "$DOVECOT_SSL_CONF"; then + sed -i "s|^\s*ssl_key\s*=.*|ssl_key = <${MAIL_KEY}|" "$DOVECOT_SSL_CONF" +else + echo "ssl_key = <${MAIL_KEY}" >> "$DOVECOT_SSL_CONF" +fi + +# Postfix-Socket-Verzeichnis sicherstellen +mkdir -p /var/spool/postfix/private +chown postfix:postfix /var/spool/postfix /var/spool/postfix/private +chmod 0755 /var/spool/postfix /var/spool/postfix/private + +# Nur aktivieren – Start/Reload erst nach App/DB in 90-services.sh +systemctl enable dovecot >/dev/null 2>&1 || true \ No newline at end of file diff --git a/scripts/60-rspamd-opendkim.sh b/scripts/60-rspamd-opendkim.sh new file mode 100644 index 0000000..2af4bf9 --- /dev/null +++ b/scripts/60-rspamd-opendkim.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail +source ./lib.sh + +log "Rspamd + OpenDKIM …" + +cat > /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 +CONF +systemctl enable --now opendkim || true \ No newline at end of file diff --git a/scripts/70-nginx.sh b/scripts/70-nginx.sh new file mode 100644 index 0000000..b69837c --- /dev/null +++ b/scripts/70-nginx.sh @@ -0,0 +1,147 @@ +#!/usr/bin/env bash +set -euo pipefail +source ./lib.sh + +log "Nginx konfigurieren …" + +NGINX_SITE="/etc/nginx/sites-available/${APP_USER}.conf" +NGINX_SITE_LINK="/etc/nginx/sites-enabled/${APP_USER}.conf" +ACME_ROOT="/var/www/letsencrypt" +install -d -m 0755 "$ACME_ROOT" + +# HTTP/2 prüfen +NGINX_HTTP2_SUFFIX="" +if nginx -V 2>&1 | grep -q http_v2; then + NGINX_HTTP2_SUFFIX=" http2" +fi + +# PHP-FPM Socket oder TCP ermitteln und fastcgi_pass bauen +detect_php_fpm_sock(){ + for v in 8.3 8.2 8.1 8.0 7.4; do + s="/run/php/php${v}-fpm.sock" + [[ -S "$s" ]] && { echo "unix:${s}"; return; } + done + [[ -S "/run/php/php-fpm.sock" ]] && { echo "unix:/run/php/php-fpm.sock"; return; } + echo "127.0.0.1:9000" +} +PHP_FPM_TARGET="$(detect_php_fpm_sock)" +if [[ "$PHP_FPM_TARGET" == unix:* ]]; then + FASTCGI_PASS="fastcgi_pass ${PHP_FPM_TARGET#unix:};" +else + FASTCGI_PASS="fastcgi_pass ${PHP_FPM_TARGET};" +fi + +# Prüfen, ob UI-Zert vorhanden ist +UI_CERT="/etc/ssl/ui/fullchain.pem" +UI_KEY="/etc/ssl/ui/privkey.pem" +SSL_ENABLED=0 +[[ -s "$UI_CERT" && -s "$UI_KEY" ]] && SSL_ENABLED=1 + +TPL="config/nginx/site.conf.tmpl" +[[ -f "$TPL" ]] || die "Nginx-Template fehlt: $TPL" +render="$(cat "$TPL")" + +# --------- Bausteine, die in das Template eingesetzt werden --------- + +# (A) HTTP-Body, wenn KEIN SSL → App direkt über Port 80 +HTTP_BODY_APP="$(cat <<'HTTP' + root ${APP_DIR}/public; + index index.php index.html; + + access_log /var/log/nginx/${APP_USER}_access.log; + error_log /var/log/nginx/${APP_USER}_error.log; + + client_max_body_size 25m; + + location / { try_files $uri $uri/ /index.php?$query_string; } + location ~ \.php$ { + include snippets/fastcgi-php.conf; + __FASTCGI_PASS__ + try_files $uri =404; + } + location ^~ /livewire/ { try_files $uri /index.php?$query_string; } + location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)$ { expires 30d; access_log off; } +HTTP +)" + +# (B) HTTP-Body, wenn SSL → nur Redirect auf 443 +HTTP_BODY_REDIRECT='return 301 https://$host$request_uri;' + +# (C) kompletter SSL-Serverblock (wird nur eingefügt, wenn SSL aktiv) +SSL_BLOCK="$(cat <<'SSL' +server { + listen 443 ssl${NGINX_HTTP2_SUFFIX}; + listen [::]:443 ssl${NGINX_HTTP2_SUFFIX}; + server_name _; + + ssl_certificate ${UI_CERT}; + ssl_certificate_key ${UI_KEY}; + ssl_protocols TLSv1.2 TLSv1.3; + + root ${APP_DIR}/public; + index index.php index.html; + + access_log /var/log/nginx/${APP_USER}_ssl_access.log; + error_log /var/log/nginx/${APP_USER}_ssl_error.log; + + client_max_body_size 25m; + + location / { try_files $uri $uri/ /index.php?$query_string; } + location ~ \.php$ { + include snippets/fastcgi-php.conf; + __FASTCGI_PASS__ + try_files $uri =404; + } + location ^~ /livewire/ { try_files $uri /index.php?$query_string; } + location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)$ { expires 30d; access_log off; } + + # WebSocket: Laravel Reverb + location /ws/ { + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + proxy_read_timeout 60s; + proxy_send_timeout 60s; + proxy_pass http://127.0.0.1:8080/; + } + + # Reverb HTTP API + location /apps/ { + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_read_timeout 60s; + proxy_send_timeout 60s; + proxy_pass http://127.0.0.1:8080/apps/; + } +} +SSL +)" + +# --------- Platzhalter ersetzen --------- +if [[ $SSL_ENABLED -eq 1 ]]; then + render="${render/__HTTP_BODY__/$HTTP_BODY_REDIRECT}" + render="${render/__SSL_SERVER_BLOCK__/$SSL_BLOCK}" +else + render="${render/__HTTP_BODY__/$HTTP_BODY_APP}" + render="${render/__SSL_SERVER_BLOCK__/}" +fi + +# Variablen & __FASTCGI_PASS__ im fertigen Render ersetzen +render="$(echo "$render" \ + | sed "s|\${APP_DIR}|${APP_DIR}|g; s|\${APP_USER}|${APP_USER}|g; \ + s|\${UI_CERT}|${UI_CERT}|g; s|\${UI_KEY}|${UI_KEY}|g; \ + s|\${NGINX_HTTP2_SUFFIX}|${NGINX_HTTP2_SUFFIX}|g; \ + s|__FASTCGI_PASS__|${FASTCGI_PASS}|g")" + +# Schreiben/aktivieren +echo "$render" > "$NGINX_SITE" +ln -sf "$NGINX_SITE" "$NGINX_SITE_LINK" + +# Test & reload +if nginx -t; then + systemctl enable --now nginx >/dev/null 2>&1 || true + systemctl reload nginx || true +else + die "nginx -t fehlgeschlagen – siehe /var/log/nginx/*.log" +fi \ No newline at end of file diff --git a/scripts/80-app.sh b/scripts/80-app.sh new file mode 100644 index 0000000..0d90928 --- /dev/null +++ b/scripts/80-app.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +set -euo pipefail +source ./lib.sh + +log "App bereitstellen …" +mkdir -p "$(dirname "$APP_DIR")" +chown -R "$APP_USER":"$APP_GROUP" "$(dirname "$APP_DIR")" + +# Repo holen oder Laravel anlegen – passe GIT_REPO/GIT_BRANCH bei Bedarf an +GIT_REPO="${GIT_REPO:-https://example.com/your-repo-placeholder.git}" +GIT_BRANCH="${GIT_BRANCH:-main}" + +if [[ "${GIT_REPO}" == "https://example.com/your-repo-placeholder.git" ]]; then + [[ -d "$APP_DIR" && -n "$(ls -A "$APP_DIR" 2>/dev/null || true)" ]] || \ + sudo -u "$APP_USER" -H bash -lc "cd /var/www && composer create-project laravel/laravel ${APP_USER} --no-interaction" +else + if [[ ! -d "${APP_DIR}/.git" ]]; then + sudo -u "$APP_USER" -H bash -lc "git clone --depth=1 -b ${GIT_BRANCH} ${GIT_REPO} ${APP_DIR}" + else + sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && git fetch --depth=1 origin ${GIT_BRANCH} && git reset --hard origin/${GIT_BRANCH}" + fi + [[ -f "${APP_DIR}/composer.json" ]] && sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && composer install --no-interaction --prefer-dist" +fi + +ENV_FILE="${APP_DIR}/.env" +sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && cp -n .env.example .env || true" +grep -q '^APP_KEY=' "$ENV_FILE" || echo "APP_KEY=" >> "$ENV_FILE" +sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan key:generate --force || true" + +# APP_URL heuristisch +APP_URL="http://${SERVER_PUBLIC_IPV4}" +UI_CERT="/etc/ssl/ui/fullchain.pem"; UI_KEY="/etc/ssl/ui/privkey.pem" +if [[ -f "$UI_CERT" && -f "$UI_KEY" && resolve_ok "$UI_HOST" ]]; then + APP_URL="https://${UI_HOST}" +fi + +upsert_env APP_NAME "${APP_NAME}" +upsert_env APP_URL "${APP_URL}" +upsert_env APP_ENV "production" +upsert_env APP_DEBUG "false" +upsert_env APP_LOCALE "${APP_LOCALE}" +upsert_env APP_FALLBACK_LOCALE "en" +upsert_env SERVER_PUBLIC_IPV4 "${SERVER_PUBLIC_IPV4}" +upsert_env SERVER_PUBLIC_IPV6 "${SERVER_PUBLIC_IPV6}" + +upsert_env BASE_DOMAIN "${BASE_DOMAIN}" +upsert_env UI_SUB "${UI_SUB}" +upsert_env WEBMAIL_SUB "${WEBMAIL_SUB}" +upsert_env SYSTEM_SUB "${SYSTEM_SUB}" +upsert_env MTA_SUB "${MTA_SUB}" + +upsert_env DB_CONNECTION "mysql" +upsert_env DB_HOST "127.0.0.1" +upsert_env DB_PORT "3306" +upsert_env DB_DATABASE "${DB_NAME}" +upsert_env DB_USERNAME "${DB_USER}" +upsert_env DB_PASSWORD "${DB_PASS}" + +upsert_env CACHE_DRIVER "redis" +upsert_env SESSION_DRIVER "redis" +upsert_env REDIS_CLIENT "phpredis" +upsert_env REDIS_HOST "127.0.0.1" +upsert_env REDIS_PORT "6379" +upsert_env REDIS_PASSWORD "${REDIS_PASS:-}" + +chown -R "$APP_USER":"$APP_GROUP" "$APP_DIR" +chmod -R u=rwX,g=rwX,o=rX "$APP_DIR" +install -d -m 0775 -o "$APP_USER" -g "$APP_GROUP" "$APP_DIR/storage" "$APP_DIR/bootstrap/cache" + +sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan optimize:clear && php artisan config:cache" \ No newline at end of file diff --git a/scripts/90-services.sh b/scripts/90-services.sh new file mode 100644 index 0000000..609f67b --- /dev/null +++ b/scripts/90-services.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +set -euo pipefail +source ./lib.sh + +log "systemd Units (Reverb / Scheduler / Queue) …" + +cat > /etc/systemd/system/${APP_USER}-ws.service < /etc/systemd/system/${APP_USER}-schedule.service < /etc/systemd/system/${APP_USER}-queue.service </dev/null 2>&1 || true +fi +systemctl enable --now ${APP_USER}-schedule +systemctl enable --now ${APP_USER}-queue + +# Web stack neu laden +systemctl reload nginx || true +systemctl restart php*-fpm || true + +# Postfix/Dovecot erst nach Migration reloaden +db_ready(){ mysql -u"${DB_USER}" -p"${DB_PASS}" -h 127.0.0.1 -D "${DB_NAME}" -e "SHOW TABLES LIKE 'migrations'\G" >/dev/null 2>&1; } +if db_ready; then + systemctl reload postfix || true + systemctl reload dovecot || true +else + echo "[i] DB noch nicht migriert – überspringe Postfix/Dovecot reload." +fi \ No newline at end of file diff --git a/scripts/95-monit.sh b/scripts/95-monit.sh new file mode 100644 index 0000000..d267ced --- /dev/null +++ b/scripts/95-monit.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +set -euo pipefail +source ./lib.sh + +log "Monit konfigurieren …" +cat > /etc/monit/monitrc <<'EOF' +set daemon 60 +set logfile syslog facility log_daemon + +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 + if failed port 465 type tcp ssl then restart + if failed port 587 type tcp then restart + +check process dovecot with pidfile /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 /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-server 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 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 +monit -t && systemctl enable --now monit +monit reload || true \ No newline at end of file diff --git a/scripts/98-motd.sh b/scripts/98-motd.sh new file mode 100644 index 0000000..1e6afea --- /dev/null +++ b/scripts/98-motd.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +set -euo pipefail +source ./lib.sh + +log "MOTD installieren …" +install -d /usr/local/bin +cat >/usr/local/bin/mw-motd <<'SH' +#!/usr/bin/env bash +set -euo pipefail +NC="\033[0m"; CY="\033[1;36m"; GR="\033[1;32m"; YE="\033[1;33m"; RD="\033[1;31m"; GY="\033[0;90m" +printf "\033[1;36m" +cat <<'ASCII' +:::: :::: ::: ::::::::::: ::: ::: ::: :::::::: ::: ::::::::::: ++:+:+: :+:+:+ :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: ++:+ +:+:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ ++#+ +:+ +#+ +#++:++#++: +#+ +#+ +#+ +:+ +#+ +#+ +:+ +#+ +#+ ++#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+#+ +#+ +#+ +#+ +#+ +#+ +#+# #+# #+# #+# #+# #+# #+#+# #+#+# #+# #+# #+# #+# +### ### ### ### ########### ########## ### ### ######## ########## ### +ASCII +printf "\033[0m\n" +now="$(date '+%Y-%m-%d %H:%M:%S %Z')" +fqdn="$(hostname -f 2>/dev/null || hostname)" +ip_int="$(hostname -I 2>/dev/null | awk '{print $1}')" +ip_ext=""; command -v curl >/dev/null 2>&1 && ip_ext="$(curl -s --max-time 1 https://ifconfig.me || true)" +upt="$(uptime -p 2>/dev/null || true)" +cores="$(nproc 2>/dev/null || echo -n '?')" +load="$(awk '{print $1" / "$2" / "$3}' /proc/loadavg 2>/dev/null)" +svc(){ systemctl is-active --quiet "$1" && echo -e "${GR}OK${NC}" || echo -e "${RD}FAIL${NC}"; } +printf "${CY}Information as of:${NC} ${YE}%s${NC}\n" "$now" +printf "${GY}FQDN :${NC} %s\n" "$fqdn" +if [ -n "$ip_ext" ]; then printf "${GY}IP :${NC} %s ${GY}(ext:${NC} %s${GY})${NC}\n" "${ip_int:-?}" "$ip_ext"; else printf "${GY}IP :${NC} %s\n" "${ip_int:-?}"; fi +printf "${GY}Uptime :${NC} %s\n" "${upt:-?}" +printf "${GY}Cores :${NC} %s\n" "$cores" +printf "${GY}Load :${NC} %s (1/5/15)\n" "${load:-?}" +printf "${GY}Svc :${NC} postfix: $(svc postfix) dovecot: $(svc dovecot) nginx: $(svc nginx) mariadb: $(svc mariadb) redis: $(svc redis)\n" +SH +chmod +x /usr/local/bin/mw-motd + +if [[ -d /etc/update-motd.d ]]; then + cat >/etc/update-motd.d/10-mailwolt <<'SH' +#!/usr/bin/env bash +/usr/local/bin/mw-motd +SH + chmod +x /etc/update-motd.d/10-mailwolt + [[ -f /etc/update-motd.d/50-motd-news ]] && chmod -x /etc/update-motd.d/50-motd-news || true +else + cat >/etc/profile.d/10-mailwolt-motd.sh <<'SH' +case "$-" in *i*) /usr/local/bin/mw-motd ;; esac +SH +fi +: > /etc/motd 2>/dev/null || true \ No newline at end of file diff --git a/scripts/99-summary.sh b/scripts/99-summary.sh new file mode 100644 index 0000000..a4f1759 --- /dev/null +++ b/scripts/99-summary.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail +source ./lib.sh + +scheme="http" +[[ -f /etc/ssl/ui/fullchain.pem && -f /etc/ssl/ui/privkey.pem ]] && scheme="https" + +echo -e " +${GREEN}${BAR}${NC} +${GREEN}✔ ${APP_NAME} Bootstrap fertig${NC} +${GREEN}${BAR}${NC} + Admin-User: ${YELLOW}${ADMIN_USER}${NC} + Admin-Mail: ${YELLOW}${ADMIN_EMAIL}${NC} + Passwort: ${RED}${ADMIN_PASS}${NC} + + Aufruf UI: ${CYAN}${scheme}://${SERVER_PUBLIC_IPV4}${NC} + App Root: ${GREY}${APP_DIR}${NC} + Nginx Site: ${GREY}/etc/nginx/sites-available/${APP_USER}.conf${NC} + Mail-FQDN: ${GREY}${MAIL_HOSTNAME}${NC} +" \ No newline at end of file diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh new file mode 100644 index 0000000..afbbbc3 --- /dev/null +++ b/scripts/bootstrap.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" +source ./lib.sh +require_root +header + +# ── Defaults ──────────────────────────────────────────────────────────────── +APP_NAME="${APP_NAME:-MailWolt}" +APP_USER="${APP_USER:-mailwolt}" +APP_GROUP="${APP_GROUP:-www-data}" +APP_USER_PREFIX="${APP_USER_PREFIX:-mw}" +APP_DIR="${APP_DIR:-/var/www/${APP_USER}}" + +BASE_DOMAIN="${BASE_DOMAIN:-example.com}" +UI_SUB="${UI_SUB:-ui}" +WEBMAIL_SUB="${WEBMAIL_SUB:-webmail}" +MTA_SUB="${MTA_SUB:-mx}" +SYSTEM_SUB="${SYSTEM_SUB:-system}" + +ADMIN_USER="${ADMIN_USER:-${APP_USER}}" +ADMIN_EMAIL="${ADMIN_EMAIL:-admin@localhost}" +ADMIN_PASS="${ADMIN_PASS:-ChangeMe}" + +DB_NAME="${DB_NAME:-${APP_USER}}" +DB_USER="${DB_USER:-${APP_USER}}" +DB_PASS="${DB_PASS:-$(openssl rand -hex 16)}" + +SERVER_PUBLIC_IPV4="$(detect_ip)" +SERVER_PUBLIC_IPV6="$(detect_ipv6)" +DEFAULT_TZ="$(detect_timezone)" +DEFAULT_LOCALE="$(guess_locale_from_tz "$DEFAULT_TZ")" + +echo -e "${GREY}Erkannte IP (v4): ${SERVER_PUBLIC_IPV4} v6: ${SERVER_PUBLIC_IPV6:-–}${NC}" + +read -r -p "Basisdomain (Enter=${BASE_DOMAIN}): " INP; BASE_DOMAIN="${INP:-$BASE_DOMAIN}" +read -r -p "UI Subdomain (Enter=${UI_SUB}): " INP; UI_SUB="${INP:-$UI_SUB}" +read -r -p "Webmail Subdomain (Enter=${WEBMAIL_SUB}): " INP; WEBMAIL_SUB="${INP:-$WEBMAIL_SUB}" +read -r -p "Mailserver Subdomain (Enter=${MTA_SUB}): " INP; MTA_SUB="${INP:-$MTA_SUB}" + +read -r -p "Zeitzone (Enter=${DEFAULT_TZ}): " INP; APP_TZ="${INP:-$DEFAULT_TZ}" +read -r -p "Sprache [de/en] (Enter=${DEFAULT_LOCALE}): " INP; APP_LOCALE="${INP:-$DEFAULT_LOCALE}" + +UI_HOST="$(join_host "$UI_SUB" "$BASE_DOMAIN")" +WEBMAIL_HOST="$(join_host "$WEBMAIL_SUB" "$BASE_DOMAIN")" +MAIL_HOSTNAME="$(join_host "$MTA_SUB" "$BASE_DOMAIN")" +SYSTEM_HOSTNAME="$(join_host "$SYSTEM_SUB" "$BASE_DOMAIN")" + +export APP_NAME APP_USER APP_GROUP APP_USER_PREFIX APP_DIR +export BASE_DOMAIN UI_SUB WEBMAIL_SUB MTA_SUB SYSTEM_SUB +export UI_HOST WEBMAIL_HOST MAIL_HOSTNAME SYSTEM_HOSTNAME +export ADMIN_USER ADMIN_EMAIL ADMIN_PASS +export DB_NAME DB_USER DB_PASS +export SERVER_PUBLIC_IPV4 SERVER_PUBLIC_IPV6 APP_TZ APP_LOCALE + +# ── Sequenz ──────────────────────────────────────────────────────────────── +for STEP in 10-provision 20-ssl 30-db 40-postfix 50-dovecot 60-rspamd-opendkim 70-nginx 80-app 90-services 95-monit 98-motd 99-summary +do + log ">>> Running ${STEP}.sh" + bash "./${STEP}.sh" +done \ No newline at end of file diff --git a/scripts/lib.sh b/scripts/lib.sh new file mode 100644 index 0000000..c9ba316 --- /dev/null +++ b/scripts/lib.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ── Styling ──────────────────────────────────────────────────────────────── +GREEN="$(printf '\033[1;32m')"; YELLOW="$(printf '\033[1;33m')" +RED="$(printf '\033[1;31m')"; CYAN="$(printf '\033[1;36m')" +GREY="$(printf '\033[0;90m')"; NC="$(printf '\033[0m')" +BAR="──────────────────────────────────────────────────────────────────────────────" +log(){ echo -e "${GREEN}[+]${NC} $*"; } +warn(){ echo -e "${YELLOW}[!]${NC} $*"; } +err(){ echo -e "${RED}[x]${NC} $*"; } +die(){ err "$*"; exit 1; } +require_root(){ [[ "$(id -u)" -eq 0 ]] || die "Bitte als root ausführen."; } + +header(){ echo -e "${CYAN}${BAR}${NC} +${CYAN} 888b d888 d8b 888 888 888 888 888 +${CYAN} 8888b d8888 Y8P 888 888 o 888 888 888 +${CYAN} 88888b.d88888 888 888 d8b 888 888 888 +${CYAN} 888Y88888P888 8888b. 888 888 888 d888b 888 .d88b. 888 888888 +${CYAN} 888 Y888P 888 '88b 888 888 888d88888b888 d88''88b 888 888 +${CYAN} 888 Y8P 888 .d888888 888 888 88888P Y88888 888 888 888 888 +${CYAN} 888 ' 888 888 888 888 888 8888P Y8888 Y88..88P 888 Y88b. +${CYAN} 888 888 'Y888888 888 888 888P Y888 'Y88P' 888 'Y888 +${CYAN}${BAR}${NC}\n"; } + +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:-}" ]] || die "Konnte Server-IP nicht ermitteln." + echo "$ip" +} +detect_ipv6(){ + local ip6 + ip6="$(ip -6 addr show scope global 2>/dev/null | awk '/inet6/{print $2}' | cut -d/ -f1 | head -n1)" || true + [[ -n "${ip6:-}" ]] || ip6="$(hostname -I 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i ~ /:/){print $i; exit}}')" || true + echo "${ip6:-}" +} +detect_timezone(){ + local tz + if command -v timedatectl >/dev/null 2>&1; then + tz="$(timedatectl show -p Timezone --value 2>/dev/null | tr -d '[:space:]')" || true + [[ -n "${tz:-}" && "$tz" == */* ]] && { echo "$tz"; return; } + fi + [[ -r /etc/timezone ]] && { tz="$(sed -n '1p' /etc/timezone | tr -d '[:space:]')" || true; [[ "$tz" == */* ]] && { echo "$tz"; return; }; } + if [[ -L /etc/localtime ]]; then + tz="$(readlink -f /etc/localtime 2>/dev/null || true)"; tz="${tz#/usr/share/zoneinfo/}" + [[ "$tz" == */* ]] && { echo "$tz"; return; } + fi + if command -v curl >/dev/null 2>&1; then + tz="$(curl -fsSL --max-time 3 https://ipapi.co/timezone 2>/dev/null || true)"; [[ "$tz" == */* ]] && { echo "$tz"; return; } + fi + echo "UTC" +} +guess_locale_from_tz(){ case "${1:-UTC}" in + Europe/Berlin|Europe/Vienna|Europe/Zurich|Europe/Luxembourg|Europe/Brussels|Europe/Amsterdam) echo "de";; + *) echo "en";; esac; } + +resolve_ok(){ local host="$1"; getent ahosts "$host" | awk '{print $1}' | sort -u | grep -q -F "${SERVER_PUBLIC_IPV4:-}" ; } +join_host(){ local sub="$1" base="$2"; [[ -z "$sub" ]] && echo "$base" || echo "$sub.$base"; } + +upsert_env(){ # upsert in $ENV_FILE + local k="$1" v="$2" ek ev + ek="$(printf '%s' "$k" | sed -e 's/[.[\*^$(){}+?|/]/\\&/g')" + ev="$(printf '%s' "$v" | sed -e 's/[&/]/\\&/g')" + if grep -qE "^[#[:space:]]*${ek}=" "$ENV_FILE" 2>/dev/null; then + sed -Ei "s|^[#[:space:]]*${ek}=.*|${k}=${ev}|g" "$ENV_FILE" + else + printf '%s=%s\n' "$k" "$v" >> "$ENV_FILE" + fi +} \ No newline at end of file