Compare commits
No commits in common. "main" and "v1.1.164" have entirely different histories.
|
|
@ -33,7 +33,7 @@ SESSION_ENCRYPT=false
|
||||||
SESSION_PATH=/
|
SESSION_PATH=/
|
||||||
# For cross-subdomain session sharing (e.g. webmail on mail.example.com):
|
# For cross-subdomain session sharing (e.g. webmail on mail.example.com):
|
||||||
# SESSION_DOMAIN=.example.com
|
# SESSION_DOMAIN=.example.com
|
||||||
SESSION_DOMAIN=
|
SESSION_DOMAIN=null
|
||||||
|
|
||||||
#BROADCAST_CONNECTION=log
|
#BROADCAST_CONNECTION=log
|
||||||
FILESYSTEM_DISK=local
|
FILESYSTEM_DISK=local
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ class WizardDomains extends Command
|
||||||
// Das Script erstellt erst die Vhosts (mit ACME-Location), dann certbot --webroot
|
// Das Script erstellt erst die Vhosts (mit ACME-Location), dann certbot --webroot
|
||||||
$helper = '/usr/local/sbin/mailwolt-apply-domains';
|
$helper = '/usr/local/sbin/mailwolt-apply-domains';
|
||||||
$out = shell_exec(sprintf(
|
$out = shell_exec(sprintf(
|
||||||
'sudo -n %s --ui-host %s --webmail-host %s --mail-host %s --ssl-auto %d',
|
'sudo -n %s --ui-host %s --webmail-host %s --mail-host %s --ssl-auto %d 2>&1',
|
||||||
escapeshellarg($helper),
|
escapeshellarg($helper),
|
||||||
escapeshellarg($ui),
|
escapeshellarg($ui),
|
||||||
escapeshellarg($webmail),
|
escapeshellarg($webmail),
|
||||||
|
|
@ -67,34 +67,27 @@ class WizardDomains extends Command
|
||||||
$ssl ? 1 : 0,
|
$ssl ? 1 : 0,
|
||||||
));
|
));
|
||||||
|
|
||||||
// Shell-Script schreibt per-Domain-Status selbst in die State-Dateien.
|
$helperOk = $out !== null && !str_contains((string) $out, '[x]');
|
||||||
// Fallback: Domains die noch auf running/pending stehen auf error setzen.
|
$outStr = (string) $out;
|
||||||
|
|
||||||
foreach (['ui', 'mail', 'webmail'] as $key) {
|
foreach (['ui', 'mail', 'webmail'] as $key) {
|
||||||
$status = trim((string) @file_get_contents(self::STATE_DIR . "/{$key}"));
|
$status = file_get_contents(self::STATE_DIR . "/{$key}");
|
||||||
if ($status === 'running' || $status === 'pending') {
|
if ($status === 'running' || $status === 'pending') {
|
||||||
file_put_contents(self::STATE_DIR . "/{$key}", 'error');
|
$domain = $domains[$key] ?? '';
|
||||||
|
if ($domain && str_contains($outStr, "[!] {$domain}:")) {
|
||||||
|
file_put_contents(self::STATE_DIR . "/{$key}", 'noipv6');
|
||||||
|
} else {
|
||||||
|
file_put_contents(self::STATE_DIR . "/{$key}", $helperOk ? 'done' : 'error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// done-Datei: Shell-Script schreibt "1"/"0"; Fallback wenn Script abstürzte.
|
file_put_contents(self::STATE_DIR . '/done', $helperOk ? '1' : '0');
|
||||||
$doneVal = trim((string) @file_get_contents(self::STATE_DIR . '/done'));
|
Setting::set('ssl_configured', $helperOk ? '1' : '0');
|
||||||
if ($doneVal === '') {
|
|
||||||
file_put_contents(self::STATE_DIR . '/done', '0');
|
|
||||||
$doneVal = '0';
|
|
||||||
}
|
|
||||||
|
|
||||||
// ssl_configured anhand tatsächlich ausgestellter LE-Zertifikate bestimmen
|
if ($helperOk && $ssl) {
|
||||||
$hasAnyCert = false;
|
$this->updateEnv(base_path('.env'), 'SESSION_SECURE_COOKIE', 'true');
|
||||||
foreach ($domains as $domain) {
|
|
||||||
if ($domain && is_dir("/etc/letsencrypt/live/{$domain}")) {
|
|
||||||
$hasAnyCert = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Setting::set('ssl_configured', $hasAnyCert ? '1' : '0');
|
|
||||||
|
|
||||||
// SESSION_SECURE_COOKIE wird nicht automatisch gesetzt —
|
|
||||||
// nginx leitet HTTP→HTTPS weiter, Secure-Flag wird im Admin gesetzt
|
|
||||||
|
|
||||||
return self::SUCCESS;
|
return self::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -169,8 +169,6 @@ class Wizard extends Component
|
||||||
|
|
||||||
public function pollSetup(): void
|
public function pollSetup(): void
|
||||||
{
|
{
|
||||||
if ($this->setupDone) return;
|
|
||||||
|
|
||||||
foreach (['ui', 'mail', 'webmail'] as $key) {
|
foreach (['ui', 'mail', 'webmail'] as $key) {
|
||||||
$file = self::STATE_DIR . "/{$key}";
|
$file = self::STATE_DIR . "/{$key}";
|
||||||
$this->domainStatus[$key] = is_readable($file)
|
$this->domainStatus[$key] = is_readable($file)
|
||||||
|
|
|
||||||
139
installer.sh
139
installer.sh
|
|
@ -76,9 +76,10 @@ footer_ok() {
|
||||||
echo -e ""
|
echo -e ""
|
||||||
echo -e " ${CYAN}➜ Setup-Wizard jetzt öffnen:${NC}"
|
echo -e " ${CYAN}➜ Setup-Wizard jetzt öffnen:${NC}"
|
||||||
echo -e " ${CYAN}http://${ip}/setup${NC}"
|
echo -e " ${CYAN}http://${ip}/setup${NC}"
|
||||||
|
echo -e " ${GREY}https://${ip}/setup${NC} (self-signed Zertifikat)"
|
||||||
echo -e ""
|
echo -e ""
|
||||||
echo -e " Laravel Root: ${GREY}${app_dir}${NC}"
|
echo -e " Laravel Root: ${GREY}${app_dir}${NC}"
|
||||||
echo -e " Mail-TLS Cert: ${GREY}${cert_dir}/{cert.pem,key.pem}${NC} (Postfix/Dovecot)"
|
echo -e " Self-signed Cert: ${GREY}${cert_dir}/{cert.pem,key.pem}${NC}"
|
||||||
echo -e " Postfix/Dovecot: ${GREY}25, 465, 587, 110, 995, 143, 993${NC}"
|
echo -e " Postfix/Dovecot: ${GREY}25, 465, 587, 110, 995, 143, 993${NC}"
|
||||||
echo -e "${GREEN}${BAR}${NC}"
|
echo -e "${GREEN}${BAR}${NC}"
|
||||||
echo
|
echo
|
||||||
|
|
@ -452,10 +453,37 @@ server {
|
||||||
access_log /var/log/nginx/${APP_USER}_access.log;
|
access_log /var/log/nginx/${APP_USER}_access.log;
|
||||||
error_log /var/log/nginx/${APP_USER}_error.log;
|
error_log /var/log/nginx/${APP_USER}_error.log;
|
||||||
|
|
||||||
location ^~ /.well-known/acme-challenge/ {
|
location / {
|
||||||
root /var/www/letsencrypt;
|
try_files \$uri \$uri/ /index.php?\$query_string;
|
||||||
try_files \$uri =404;
|
|
||||||
}
|
}
|
||||||
|
location ~ \.php\$ {
|
||||||
|
include snippets/fastcgi-php.conf;
|
||||||
|
fastcgi_pass unix:${PHP_FPM_SOCK};
|
||||||
|
}
|
||||||
|
location ^~ /livewire/ {
|
||||||
|
try_files \$uri /index.php?\$query_string;
|
||||||
|
}
|
||||||
|
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)\$ {
|
||||||
|
expires 30d;
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
listen [::]:443 ssl http2;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
ssl_certificate ${CERT};
|
||||||
|
ssl_certificate_key ${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;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
try_files \$uri \$uri/ /index.php?\$query_string;
|
try_files \$uri \$uri/ /index.php?\$query_string;
|
||||||
}
|
}
|
||||||
|
|
@ -672,17 +700,9 @@ if [ "${SSL_AUTO}" = "1" ]; then
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- Phase 3: Finale Vhosts ---
|
# --- Phase 3: Finale Vhosts (LE-Cert oder self-signed Fallback) ---
|
||||||
# Nur HTTPS wenn LE-Cert tatsächlich vorhanden, sonst HTTP-only (kein self-signed Fallback)
|
|
||||||
UI_HAS_CERT=0
|
|
||||||
WM_HAS_CERT=0
|
|
||||||
[ -f "/etc/letsencrypt/live/${UI_HOST}/fullchain.pem" ] && UI_HAS_CERT=1
|
|
||||||
[ -f "/etc/letsencrypt/live/${WEBMAIL_HOST}/fullchain.pem" ] && WM_HAS_CERT=1
|
|
||||||
(
|
(
|
||||||
|
cat <<CONF
|
||||||
if [ "${UI_HAS_CERT}" = "1" ] || [ "${WM_HAS_CERT}" = "1" ]; then
|
|
||||||
# Mindestens ein Cert vorhanden → HTTP-Redirect Block
|
|
||||||
cat <<CONF
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
listen [::]:80;
|
listen [::]:80;
|
||||||
|
|
@ -695,105 +715,74 @@ server {
|
||||||
location / { return 301 https://\$host\$request_uri; }
|
location / { return 301 https://\$host\$request_uri; }
|
||||||
}
|
}
|
||||||
CONF
|
CONF
|
||||||
else
|
|
||||||
# Kein Cert → HTTP-only, App läuft auf Port 80 weiter
|
|
||||||
cat <<CONF
|
|
||||||
server {
|
|
||||||
listen 80 default_server;
|
|
||||||
listen [::]:80 default_server;
|
|
||||||
server_name _;
|
|
||||||
|
|
||||||
root ${APP_DIR}/public;
|
if [ -n "${UI_HOST}" ]; then
|
||||||
index index.php index.html;
|
if [ "${SSL_AUTO}" = "1" ] && [ -f "/etc/letsencrypt/live/${UI_HOST}/fullchain.pem" ]; then
|
||||||
|
CERT_UI="/etc/letsencrypt/live/${UI_HOST}/fullchain.pem"
|
||||||
location /.well-known/acme-challenge/ {
|
KEY_UI="/etc/letsencrypt/live/${UI_HOST}/privkey.pem"
|
||||||
root ${ACME_ROOT};
|
else
|
||||||
try_files \$uri =404;
|
CERT_UI="/etc/mailwolt/ssl/cert.pem"
|
||||||
}
|
KEY_UI="/etc/mailwolt/ssl/key.pem"
|
||||||
location / { try_files \$uri \$uri/ /index.php?\$query_string; }
|
fi
|
||||||
location ~ \.php\$ {
|
|
||||||
include snippets/fastcgi-php.conf;
|
|
||||||
fastcgi_pass unix:${PHP_FPM_SOCK};
|
|
||||||
}
|
|
||||||
location ^~ /livewire/ { try_files \$uri /index.php?\$query_string; }
|
|
||||||
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)\$ { expires 30d; access_log off; }
|
|
||||||
}
|
|
||||||
CONF
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "${UI_HOST}" ] && [ "${UI_HAS_CERT}" = "1" ]; then
|
|
||||||
cat <<CONF
|
cat <<CONF
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 443 ssl;
|
listen 443 ssl http2;
|
||||||
listen [::]:443 ssl;
|
listen [::]:443 ssl http2;
|
||||||
http2 on;
|
|
||||||
server_name ${UI_HOST};
|
server_name ${UI_HOST};
|
||||||
|
|
||||||
ssl_certificate /etc/letsencrypt/live/${UI_HOST}/fullchain.pem;
|
ssl_certificate ${CERT_UI};
|
||||||
ssl_certificate_key /etc/letsencrypt/live/${UI_HOST}/privkey.pem;
|
ssl_certificate_key ${KEY_UI};
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
|
||||||
root ${APP_DIR}/public;
|
root ${APP_DIR}/public;
|
||||||
index index.php index.html;
|
index index.php index.html;
|
||||||
|
|
||||||
location / { try_files \$uri \$uri/ /index.php?\$query_string; }
|
location / { try_files \$uri \$uri/ /index.php?\$query_string; }
|
||||||
location ~ \.php\$ {
|
location ~ \.php$ {
|
||||||
include snippets/fastcgi-php.conf;
|
include snippets/fastcgi-php.conf;
|
||||||
fastcgi_param HTTPS on;
|
|
||||||
fastcgi_pass unix:${PHP_FPM_SOCK};
|
fastcgi_pass unix:${PHP_FPM_SOCK};
|
||||||
}
|
}
|
||||||
location ^~ /livewire/ { try_files \$uri /index.php?\$query_string; }
|
location ^~ /livewire/ { try_files \$uri /index.php?\$query_string; }
|
||||||
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)\$ { expires 30d; access_log off; }
|
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)$ { expires 30d; access_log off; }
|
||||||
}
|
}
|
||||||
CONF
|
CONF
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "${WEBMAIL_HOST}" ] && [ "${WM_HAS_CERT}" = "1" ]; then
|
if [ -n "${WEBMAIL_HOST}" ]; then
|
||||||
|
if [ "${SSL_AUTO}" = "1" ] && [ -f "/etc/letsencrypt/live/${WEBMAIL_HOST}/fullchain.pem" ]; then
|
||||||
|
CERT_WM="/etc/letsencrypt/live/${WEBMAIL_HOST}/fullchain.pem"
|
||||||
|
KEY_WM="/etc/letsencrypt/live/${WEBMAIL_HOST}/privkey.pem"
|
||||||
|
else
|
||||||
|
CERT_WM="/etc/mailwolt/ssl/cert.pem"
|
||||||
|
KEY_WM="/etc/mailwolt/ssl/key.pem"
|
||||||
|
fi
|
||||||
cat <<CONF
|
cat <<CONF
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 443 ssl;
|
listen 443 ssl http2;
|
||||||
listen [::]:443 ssl;
|
listen [::]:443 ssl http2;
|
||||||
http2 on;
|
|
||||||
server_name ${WEBMAIL_HOST};
|
server_name ${WEBMAIL_HOST};
|
||||||
|
|
||||||
ssl_certificate /etc/letsencrypt/live/${WEBMAIL_HOST}/fullchain.pem;
|
ssl_certificate ${CERT_WM};
|
||||||
ssl_certificate_key /etc/letsencrypt/live/${WEBMAIL_HOST}/privkey.pem;
|
ssl_certificate_key ${KEY_WM};
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
|
||||||
root ${APP_DIR}/public;
|
root ${APP_DIR}/public;
|
||||||
index index.php index.html;
|
index index.php index.html;
|
||||||
|
|
||||||
location / { try_files \$uri \$uri/ /index.php?\$query_string; }
|
location / { try_files \$uri \$uri/ /index.php?\$query_string; }
|
||||||
location ~ \.php\$ {
|
location ~ \.php$ {
|
||||||
include snippets/fastcgi-php.conf;
|
include snippets/fastcgi-php.conf;
|
||||||
fastcgi_param HTTPS on;
|
|
||||||
fastcgi_pass unix:${PHP_FPM_SOCK};
|
fastcgi_pass unix:${PHP_FPM_SOCK};
|
||||||
}
|
}
|
||||||
location ^~ /livewire/ { try_files \$uri /index.php?\$query_string; }
|
location ^~ /livewire/ { try_files \$uri /index.php?\$query_string; }
|
||||||
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)\$ { expires 30d; access_log off; }
|
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)$ { expires 30d; access_log off; }
|
||||||
}
|
}
|
||||||
CONF
|
CONF
|
||||||
fi
|
fi
|
||||||
) > "${NGINX_SITE}"
|
) > "${NGINX_SITE}"
|
||||||
|
|
||||||
# State-Dateien VOR dem nginx-Switch schreiben:
|
|
||||||
# Browser-Poll (alle 2s) liest done=1 → Polling stoppt → "Zum Login" erscheint.
|
|
||||||
# Danach 6s sleep → nginx switchet auf HTTPS → User klickt Link → funktioniert.
|
|
||||||
STATE_DIR="/var/lib/mailwolt/wizard"
|
|
||||||
if [ -d "${STATE_DIR}" ]; then
|
|
||||||
for k in ui mail webmail; do
|
|
||||||
[ -f "${STATE_DIR}/${k}" ] && printf "done" > "${STATE_DIR}/${k}"
|
|
||||||
done
|
|
||||||
if [ "${UI_HAS_CERT}" = "1" ] || [ "${WM_HAS_CERT}" = "1" ]; then
|
|
||||||
printf "1" > "${STATE_DIR}/done"
|
|
||||||
else
|
|
||||||
printf "0" > "${STATE_DIR}/done"
|
|
||||||
fi
|
|
||||||
sleep 6
|
|
||||||
fi
|
|
||||||
|
|
||||||
nginx -t && systemctl reload nginx
|
nginx -t && systemctl reload nginx
|
||||||
HELPER
|
HELPER
|
||||||
chmod 755 /usr/local/sbin/mailwolt-apply-domains
|
chmod 755 /usr/local/sbin/mailwolt-apply-domains
|
||||||
|
|
@ -805,7 +794,6 @@ install -m 755 "${APP_DIR}/update.sh" /usr/local/sbin/mailwolt-update
|
||||||
cat > /etc/sudoers.d/mailwolt-certbot <<'SUDOERS'
|
cat > /etc/sudoers.d/mailwolt-certbot <<'SUDOERS'
|
||||||
www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-apply-domains
|
www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-apply-domains
|
||||||
www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-update
|
www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-update
|
||||||
www-data ALL=(root) NOPASSWD: /usr/bin/certbot
|
|
||||||
SUDOERS
|
SUDOERS
|
||||||
chmod 440 /etc/sudoers.d/mailwolt-certbot
|
chmod 440 /etc/sudoers.d/mailwolt-certbot
|
||||||
|
|
||||||
|
|
@ -881,7 +869,7 @@ check process dovecot with pidfile /run/dovecot/master.pid
|
||||||
if failed host 127.0.0.1 port 993 type tcpssl for 3 cycles then restart
|
if failed host 127.0.0.1 port 993 type tcpssl for 3 cycles then restart
|
||||||
if 5 restarts within 10 cycles then alert
|
if 5 restarts within 10 cycles then alert
|
||||||
|
|
||||||
check process mariadb matching "mysqld"
|
check process mariadb with pidfile /run/mysqld/mysqld.pid
|
||||||
start program = "/bin/systemctl start mariadb"
|
start program = "/bin/systemctl start mariadb"
|
||||||
stop program = "/bin/systemctl stop mariadb"
|
stop program = "/bin/systemctl stop mariadb"
|
||||||
if failed host 127.0.0.1 port 3306 type tcp for 2 cycles then restart
|
if failed host 127.0.0.1 port 3306 type tcp for 2 cycles then restart
|
||||||
|
|
@ -915,6 +903,7 @@ check process nginx with pidfile /run/nginx.pid
|
||||||
start program = "/bin/systemctl start nginx"
|
start program = "/bin/systemctl start nginx"
|
||||||
stop program = "/bin/systemctl stop nginx"
|
stop program = "/bin/systemctl stop nginx"
|
||||||
if failed host 127.0.0.1 port 80 type tcp for 2 cycles then restart
|
if failed host 127.0.0.1 port 80 type tcp for 2 cycles then restart
|
||||||
|
if failed host 127.0.0.1 port 443 type tcpssl for 2 cycles then restart
|
||||||
if 5 restarts within 10 cycles then alert
|
if 5 restarts within 10 cycles then alert
|
||||||
|
|
||||||
check process fail2ban with pidfile /run/fail2ban/fail2ban.pid
|
check process fail2ban with pidfile /run/fail2ban/fail2ban.pid
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AgentMigrationStateService">
|
||||||
|
<option name="migrationStatus" value="COMPLETED" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AskMigrationStateService">
|
||||||
|
<option name="migrationStatus" value="COMPLETED" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Ask2AgentMigrationStateService">
|
||||||
|
<option name="migrationStatus" value="COMPLETED" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="EditMigrationStateService">
|
||||||
|
<option name="migrationStatus" value="COMPLETED" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="MarkdownSettingsMigration">
|
||||||
|
<option name="stateVersion" value="1" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/mailwolt-installer.iml" filepath="$PROJECT_DIR$/.idea/mailwolt-installer.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="MessDetectorOptionsConfiguration">
|
||||||
|
<option name="transferred" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PHPCSFixerOptionsConfiguration">
|
||||||
|
<option name="transferred" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PHPCodeSnifferOptionsConfiguration">
|
||||||
|
<option name="highlightLevel" value="WARNING" />
|
||||||
|
<option name="transferred" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PhpStanOptionsConfiguration">
|
||||||
|
<option name="transferred" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PsalmOptionsConfiguration">
|
||||||
|
<option name="transferred" value="true" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -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__
|
||||||
|
|
@ -0,0 +1,909 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
(
|
||||||
|
export HISTFILE=
|
||||||
|
set +o history
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
##############################################
|
||||||
|
# MailWolt #
|
||||||
|
# Bootstrap Installer v1.0 #
|
||||||
|
##############################################
|
||||||
|
|
||||||
|
# ===== CLI-Flags (-dev / -stag) =====
|
||||||
|
APP_ENV="${APP_ENV:-production}"
|
||||||
|
APP_DEBUG="${APP_DEBUG:-false}"
|
||||||
|
|
||||||
|
DEV_MODE=0
|
||||||
|
STAG_MODE=0
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-dev)
|
||||||
|
DEV_MODE=1
|
||||||
|
APP_ENV="local"
|
||||||
|
APP_DEBUG="true"
|
||||||
|
;;
|
||||||
|
-stag|-staging)
|
||||||
|
STAG_MODE=1
|
||||||
|
APP_ENV="staging"
|
||||||
|
APP_DEBUG="false"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
# ===== Branding & Pfade =====
|
||||||
|
APP_NAME="${APP_NAME:-MailWolt}"
|
||||||
|
|
||||||
|
APP_USER="${APP_USER:-mailwolt}"
|
||||||
|
APP_GROUP="${APP_GROUP:-www-data}"
|
||||||
|
|
||||||
|
APP_DIR="/var/www/${APP_USER}"
|
||||||
|
|
||||||
|
ADMIN_USER="${APP_USER}"
|
||||||
|
ADMIN_EMAIL="admin@localhost"
|
||||||
|
ADMIN_PASS="ChangeMe"
|
||||||
|
|
||||||
|
CONF_BASE="/etc/${APP_USER}"
|
||||||
|
CERT_DIR="${CONF_BASE}/ssl"
|
||||||
|
CERT="${CERT_DIR}/cert.pem"
|
||||||
|
KEY="${CERT_DIR}/key.pem"
|
||||||
|
|
||||||
|
NGINX_SITE="/etc/nginx/sites-available/${APP_USER}.conf"
|
||||||
|
NGINX_SITE_LINK="/etc/nginx/sites-enabled/${APP_USER}.conf"
|
||||||
|
|
||||||
|
DB_NAME="${DB_NAME:-${APP_USER}}"
|
||||||
|
DB_USER="${DB_USER:-${APP_USER}}"
|
||||||
|
DB_PASS="${DB_PASS:-$(openssl rand -hex 16)}"
|
||||||
|
|
||||||
|
GIT_REPO="${GIT_REPO:-http://10.10.20.81:3000/boban/mailwolt.git}"
|
||||||
|
GIT_BRANCH="${GIT_BRANCH:-main}"
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
print_bootstrap_summary() {
|
||||||
|
local ip="$1"
|
||||||
|
local admin_user="$2"
|
||||||
|
local admin_pass="$3"
|
||||||
|
local GREEN="\033[1;32m"
|
||||||
|
local CYAN="\033[1;36m"
|
||||||
|
local GREY="\033[0;90m"
|
||||||
|
local YELLOW="\033[1;33m"
|
||||||
|
local RED="\033[1;31m"
|
||||||
|
local NC="\033[0m"
|
||||||
|
local BAR="${BAR:-──────────────────────────────────────────────────────────────────────────────}"
|
||||||
|
local scheme="http"
|
||||||
|
if [ -s "${CERT}" ] && [ -s "${KEY}" ]; then scheme="https"; fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo -e "${GREEN}${BAR}${NC}"
|
||||||
|
echo -e "${GREEN} ✔ ${APP_NAME} Bootstrap erfolgreich abgeschlossen${NC}"
|
||||||
|
echo -e "${GREEN}${BAR}${NC}"
|
||||||
|
echo -e " Bootstrap-Login (nur für ERSTEN Login & Wizard):"
|
||||||
|
echo -e " User: ${YELLOW}${admin_user}${NC}"
|
||||||
|
echo -e " Passwort: ${RED}${admin_pass}${NC}"
|
||||||
|
echo
|
||||||
|
echo -e " Aufruf: ${CYAN}${scheme}://${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; }; }
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
gen() { head -c 512 /dev/urandom | tr -dc 'A-Za-z0-9' | head -c "${1:-28}" || true; }
|
||||||
|
pw() { gen 28; }
|
||||||
|
short() { gen 16; }
|
||||||
|
|
||||||
|
# ===== Start =====
|
||||||
|
require_root
|
||||||
|
header
|
||||||
|
|
||||||
|
SERVER_IP="$(detect_ip)"
|
||||||
|
MAIL_HOSTNAME="${MAIL_HOSTNAME:-"bootstrap.local"}"
|
||||||
|
TZ="${TZ:-""}"
|
||||||
|
|
||||||
|
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 ----
|
||||||
|
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)"
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
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 \
|
||||||
|
fail2ban \
|
||||||
|
ca-certificates rsyslog sudo openssl netcat-openbsd monit acl
|
||||||
|
|
||||||
|
NGINX_HTTP2_SUPPORTED=0
|
||||||
|
if nginx -V 2>&1 | grep -q http_v2; then
|
||||||
|
NGINX_HTTP2_SUPPORTED=1
|
||||||
|
log "Nginx: HTTP/2-Unterstützung vorhanden ✅"
|
||||||
|
else
|
||||||
|
warn "Nginx: HTTP/2-Modul nicht gefunden – wechsle auf 'nginx-full'…"
|
||||||
|
apt-get install -y nginx-full || true
|
||||||
|
systemctl restart nginx || true
|
||||||
|
if nginx -V 2>&1 | grep -q http_v2; then
|
||||||
|
NGINX_HTTP2_SUPPORTED=1
|
||||||
|
log "Nginx: HTTP/2 jetzt verfügbar ✅"
|
||||||
|
else
|
||||||
|
warn "HTTP/2 weiterhin nicht verfügbar (verwende SSL ohne http2)."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$NGINX_HTTP2_SUPPORTED" = "1" ]; then
|
||||||
|
NGINX_HTTP2_SUFFIX=" http2"
|
||||||
|
else
|
||||||
|
NGINX_HTTP2_SUFFIX=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ===== Verzeichnisse / User =====
|
||||||
|
log "Verzeichnisse und Benutzer anlegen…"
|
||||||
|
mkdir -p "${CERT_DIR}" /etc/postfix/sql /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 "$APP_GROUP" "$APP_USER"
|
||||||
|
|
||||||
|
# ===== Self-signed TLS (SAN = IP) =====
|
||||||
|
OSSL_CFG="${CERT_DIR}/openssl.cnf"
|
||||||
|
if [ ! -s "$CERT" ] || [ ! -s "$KEY" ]; then
|
||||||
|
log "Erzeuge Self-Signed TLS Zertifikat (SAN=IP:${SERVER_IP})…"
|
||||||
|
install -d -m 0750 -o root -g "${APP_USER}" "${CERT_DIR}"
|
||||||
|
cat > "$OSSL_CFG" <<CFG
|
||||||
|
[req]
|
||||||
|
default_bits = 2048
|
||||||
|
prompt = no
|
||||||
|
default_md = sha256
|
||||||
|
req_extensions = req_ext
|
||||||
|
distinguished_name = dn
|
||||||
|
|
||||||
|
[dn]
|
||||||
|
CN = ${SERVER_IP}
|
||||||
|
O = ${APP_NAME}
|
||||||
|
C = DE
|
||||||
|
|
||||||
|
[req_ext]
|
||||||
|
subjectAltName = @alt_names
|
||||||
|
|
||||||
|
[alt_names]
|
||||||
|
IP.1 = ${SERVER_IP}
|
||||||
|
CFG
|
||||||
|
openssl req -x509 -newkey rsa:2048 -days 825 -nodes \
|
||||||
|
-keyout "$KEY" -out "$CERT" -config "$OSSL_CFG"
|
||||||
|
chown root:"${APP_USER}" "$KEY" "$CERT"
|
||||||
|
chmod 640 "$KEY" "$CERT"
|
||||||
|
chmod 750 "${CERT_DIR}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
DEV_USER="${SUDO_USER:-$USER}"
|
||||||
|
if command -v setfacl >/dev/null 2>&1; then
|
||||||
|
setfacl -m u:${DEV_USER}:x "${CONF_BASE}" "${CERT_DIR}" || true
|
||||||
|
setfacl -m u:${DEV_USER}:r "$CERT" "$KEY" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ===== MariaDB =====
|
||||||
|
log "MariaDB vorbereiten…"
|
||||||
|
systemctl enable --now mariadb
|
||||||
|
mysql -uroot <<SQL
|
||||||
|
CREATE DATABASE IF NOT EXISTS ${DB_NAME}
|
||||||
|
CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
CREATE USER IF NOT EXISTS '${DB_USER}'@'localhost';
|
||||||
|
CREATE USER IF NOT EXISTS '${DB_USER}'@'127.0.0.1';
|
||||||
|
ALTER USER '${DB_USER}'@'localhost' IDENTIFIED BY '${DB_PASS}';
|
||||||
|
ALTER USER '${DB_USER}'@'127.0.0.1' IDENTIFIED BY '${DB_PASS}';
|
||||||
|
GRANT ALL PRIVILEGES ON ${DB_NAME}.* TO '${DB_USER}'@'localhost';
|
||||||
|
GRANT ALL PRIVILEGES ON ${DB_NAME}.* TO '${DB_USER}'@'127.0.0.1';
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
|
SQL
|
||||||
|
|
||||||
|
# ===== Postfix =====
|
||||||
|
postconf -e "myhostname = ${MAIL_HOSTNAME}"
|
||||||
|
postconf -e "myorigin = \$myhostname"
|
||||||
|
postconf -e "mydestination = "
|
||||||
|
postconf -e "inet_interfaces = all"
|
||||||
|
postconf -e "inet_protocols = ipv4"
|
||||||
|
postconf -e "smtpd_banner = \$myhostname ESMTP"
|
||||||
|
postconf -e "smtpd_tls_cert_file = ${CERT}"
|
||||||
|
postconf -e "smtpd_tls_key_file = ${KEY}"
|
||||||
|
postconf -e "smtpd_tls_security_level = may"
|
||||||
|
postconf -e "smtp_tls_security_level = may"
|
||||||
|
postconf -e "smtpd_tls_received_header = yes"
|
||||||
|
postconf -e "disable_vrfy_command = yes"
|
||||||
|
postconf -e "smtpd_helo_required = yes"
|
||||||
|
postconf -e "milter_default_action = accept"
|
||||||
|
postconf -e "milter_protocol = 6"
|
||||||
|
postconf -e "smtpd_milters = inet:127.0.0.1:11332, inet:127.0.0.1:8891"
|
||||||
|
postconf -e "non_smtpd_milters = inet:127.0.0.1:11332, inet:127.0.0.1:8891"
|
||||||
|
postconf -e "smtpd_sasl_type = dovecot"
|
||||||
|
postconf -e "smtpd_sasl_path = private/auth"
|
||||||
|
postconf -e "smtpd_sasl_auth_enable = yes"
|
||||||
|
postconf -e "smtpd_sasl_security_options = noanonymous"
|
||||||
|
postconf -e "smtpd_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination"
|
||||||
|
postconf -e "smtpd_relay_restrictions = permit_mynetworks, reject_unauth_destination"
|
||||||
|
postconf -M "smtp/inet=smtp inet n - n - - smtpd -o smtpd_peername_lookup=no -o smtpd_timeout=30s"
|
||||||
|
postconf -M "submission/inet=submission inet n - n - - smtpd -o syslog_name=postfix/submission -o smtpd_peername_lookup=no -o smtpd_tls_security_level=encrypt -o smtpd_tls_auth_only=yes -o smtpd_sasl_auth_enable=yes -o smtpd_relay_restrictions=permit_sasl_authenticated,reject -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject"
|
||||||
|
postconf -M "smtps/inet=smtps inet n - n - - smtpd -o syslog_name=postfix/smtps -o smtpd_peername_lookup=no -o smtpd_tls_wrappermode=yes -o smtpd_tls_auth_only=yes -o smtpd_sasl_auth_enable=yes -o smtpd_relay_restrictions=permit_sasl_authenticated,reject -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject"
|
||||||
|
postconf -M "pickup/unix=pickup unix n - y 60 1 pickup"
|
||||||
|
postconf -M "cleanup/unix=cleanup unix n - y - 0 cleanup"
|
||||||
|
postconf -M "qmgr/unix=qmgr unix n - n 300 1 qmgr"
|
||||||
|
|
||||||
|
install -d -o root -g postfix -m 750 /etc/postfix/sql
|
||||||
|
install -o root -g postfix -m 640 /dev/null /etc/postfix/sql/mysql-virtual-mailbox-maps.cf
|
||||||
|
cat > /etc/postfix/sql/mysql-virtual-mailbox-maps.cf <<CONF
|
||||||
|
hosts = 127.0.0.1
|
||||||
|
user = ${DB_USER}
|
||||||
|
password = ${DB_PASS}
|
||||||
|
dbname = ${DB_NAME}
|
||||||
|
# query = SELECT 1 FROM mail_users u JOIN domains d ON d.id = u.domain_id WHERE u.email = '%s' AND u.is_active = 1 AND d.is_active = 1 LIMIT 1;
|
||||||
|
CONF
|
||||||
|
chown root:postfix /etc/postfix/sql/mysql-virtual-mailbox-maps.cf
|
||||||
|
chmod 640 /etc/postfix/sql/mysql-virtual-mailbox-maps.cf
|
||||||
|
|
||||||
|
install -o root -g postfix -m 640 /dev/null /etc/postfix/sql/mysql-virtual-alias-maps.cf
|
||||||
|
cat > /etc/postfix/sql/mysql-virtual-alias-maps.cf <<CONF
|
||||||
|
hosts = 127.0.0.1
|
||||||
|
user = ${DB_USER}
|
||||||
|
password = ${DB_PASS}
|
||||||
|
dbname = ${DB_NAME}
|
||||||
|
# query = SELECT destination FROM mail_aliases a JOIN domains d ON d.id = a.domain_id WHERE a.source = '%s' AND a.is_active = 1 AND d.is_active = 1 LIMIT 1;
|
||||||
|
CONF
|
||||||
|
chown root:postfix /etc/postfix/sql/mysql-virtual-alias-maps.cf
|
||||||
|
chmod 640 /etc/postfix/sql/mysql-virtual-alias-maps.cf
|
||||||
|
|
||||||
|
systemctl restart postfix
|
||||||
|
systemctl enable --now postfix
|
||||||
|
|
||||||
|
# ===== Dovecot =====
|
||||||
|
log "Dovecot konfigurieren…"
|
||||||
|
cat > /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 <<CONF
|
||||||
|
driver = mysql
|
||||||
|
connect = host=127.0.0.1 dbname=${DB_NAME} user=${DB_USER} password=${DB_PASS}
|
||||||
|
default_pass_scheme = BLF-CRYPT
|
||||||
|
# password_query = SELECT email AS user, password_hash AS password FROM mail_users WHERE email = '%u' AND is_active = 1 LIMIT 1;
|
||||||
|
CONF
|
||||||
|
chown root:dovecot /etc/dovecot/dovecot-sql.conf.ext
|
||||||
|
chmod 640 /etc/dovecot/dovecot-sql.conf.ext
|
||||||
|
|
||||||
|
cat > /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
|
||||||
|
sudo chown root:dovecot /etc/dovecot/conf.d/auth-sql.conf.ext
|
||||||
|
sudo chmod 640 /etc/dovecot/conf.d/auth-sql.conf.ext
|
||||||
|
|
||||||
|
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 <<CONF
|
||||||
|
ssl = required
|
||||||
|
ssl_cert = <${CERT}
|
||||||
|
ssl_key = <${KEY}
|
||||||
|
CONF
|
||||||
|
|
||||||
|
sudo mkdir -p /var/spool/postfix/private
|
||||||
|
sudo chown postfix:postfix /var/spool/postfix /var/spool/postfix/private
|
||||||
|
sudo chmod 0755 /var/spool/postfix /var/spool/postfix/private
|
||||||
|
|
||||||
|
sudo systemctl restart dovecot
|
||||||
|
sudo systemctl restart postfix
|
||||||
|
systemctl enable --now dovecot
|
||||||
|
|
||||||
|
# ===== Rspamd & OpenDKIM =====
|
||||||
|
log "Rspamd + OpenDKIM aktivieren…"
|
||||||
|
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
|
||||||
|
# KeyTable / SigningTable werden nach dem Wizard gesetzt
|
||||||
|
CONF
|
||||||
|
systemctl enable --now opendkim || true
|
||||||
|
|
||||||
|
# ===== Redis =====
|
||||||
|
log "Redis absichern (Passwort setzen & nur localhost)…"
|
||||||
|
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"
|
||||||
|
if grep -qE '^\s*#?\s*requirepass ' "$REDIS_CONF"; then
|
||||||
|
sed -i "s/^\s*#\?\s*requirepass .*/requirepass ${REDIS_PASS}/" "$REDIS_CONF"
|
||||||
|
else
|
||||||
|
printf "\nrequirepass %s\n" "${REDIS_PASS}" >> "$REDIS_CONF"
|
||||||
|
fi
|
||||||
|
systemctl enable --now redis-server
|
||||||
|
systemctl restart redis-server
|
||||||
|
|
||||||
|
# ===== Nginx =====
|
||||||
|
log "Nginx konfigurieren…"
|
||||||
|
rm -f /etc/nginx/sites-enabled/default /etc/nginx/sites-available/default || true
|
||||||
|
|
||||||
|
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 "$s"; return; }; done
|
||||||
|
[ -S "/run/php/php-fpm.sock" ] && { echo "/run/php/php-fpm.sock"; return; }
|
||||||
|
echo "127.0.0.1:9000"
|
||||||
|
}
|
||||||
|
PHP_FPM_SOCK="$(detect_php_fpm_sock)"
|
||||||
|
install -d -m 0755 /var/www/letsencrypt
|
||||||
|
|
||||||
|
if [ -s "${CERT}" ] && [ -s "${KEY}" ]; then
|
||||||
|
cat > "${NGINX_SITE}" <<CONF
|
||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
listen [::]:80 default_server;
|
||||||
|
server_name _;
|
||||||
|
location ^~ /.well-known/acme-challenge/ { root /var/www/letsencrypt; allow all; }
|
||||||
|
return 301 https://\$host\$request_uri;
|
||||||
|
}
|
||||||
|
server {
|
||||||
|
listen 443 ssl${NGINX_HTTP2_SUFFIX};
|
||||||
|
listen [::]:443 ssl${NGINX_HTTP2_SUFFIX};
|
||||||
|
server_name _;
|
||||||
|
ssl_certificate ${CERT};
|
||||||
|
ssl_certificate_key ${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 unix:${PHP_FPM_SOCK}; }
|
||||||
|
location ^~ /livewire/ { try_files \$uri /index.php?\$query_string; }
|
||||||
|
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)\$ { expires 30d; access_log off; }
|
||||||
|
location /ws {
|
||||||
|
proxy_pass http://127.0.0.1:8080;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CONF
|
||||||
|
else
|
||||||
|
cat > "${NGINX_SITE}" <<CONF
|
||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
listen [::]:80 default_server;
|
||||||
|
server_name _;
|
||||||
|
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 unix:${PHP_FPM_SOCK}; }
|
||||||
|
location ^~ /livewire/ { try_files \$uri /index.php?\$query_string; }
|
||||||
|
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)\$ { expires 30d; access_log off; }
|
||||||
|
}
|
||||||
|
CONF
|
||||||
|
fi
|
||||||
|
|
||||||
|
ln -sf "${NGINX_SITE}" "${NGINX_SITE_LINK}"
|
||||||
|
if nginx -t; then
|
||||||
|
systemctl enable --now nginx
|
||||||
|
systemctl reload nginx || true
|
||||||
|
else
|
||||||
|
echo "[x] Nginx-Konfiguration fehlerhaft – bitte /var/log/nginx/* prüfen."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ===== Laravel Projekt =====
|
||||||
|
log "Laravel bereitstellen…"
|
||||||
|
mkdir -p "$(dirname "$APP_DIR")"
|
||||||
|
chown -R "$APP_USER":"$APP_GROUP" "$(dirname "$APP_DIR")"
|
||||||
|
|
||||||
|
log "Git Repo vorbereiten…"
|
||||||
|
if [ "${GIT_REPO}" = "https://example.com/your-repo-placeholder.git" ]; then
|
||||||
|
if [ ! -d "${APP_DIR}" ] || [ -z "$(ls -A "$APP_DIR" 2>/dev/null || true)" ]; then
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "cd /var/www && COMPOSER_ALLOW_SUPERUSER=0 composer create-project laravel/laravel ${APP_USER} --no-interaction"
|
||||||
|
fi
|
||||||
|
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 "
|
||||||
|
set -e
|
||||||
|
cd ${APP_DIR}
|
||||||
|
git checkout ${GIT_BRANCH} 2>/dev/null || git checkout -B ${GIT_BRANCH}
|
||||||
|
git fetch --depth=1 origin ${GIT_BRANCH}
|
||||||
|
if git merge-base --is-ancestor HEAD origin/${GIT_BRANCH}; then
|
||||||
|
git pull --ff-only
|
||||||
|
else
|
||||||
|
echo '[i] Non-fast-forward erkannt – setze hart auf origin/${GIT_BRANCH}.' >&2
|
||||||
|
git reset --hard origin/${GIT_BRANCH}
|
||||||
|
git clean -fd
|
||||||
|
fi
|
||||||
|
"
|
||||||
|
fi
|
||||||
|
if [ -f "${APP_DIR}/composer.json" ]; then
|
||||||
|
if [ "${DEV_MODE}" = "1" ]; then
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && composer install --no-interaction --prefer-dist"
|
||||||
|
else
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && composer install --no-interaction --prefer-dist --no-dev"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ===== Node / Frontend =====
|
||||||
|
if [ -f "${APP_DIR}/package.json" ]; then
|
||||||
|
log "Node/NPM installieren…"
|
||||||
|
if command -v node >/dev/null 2>&1; then
|
||||||
|
NODE_MAJ=$(node -v | sed 's/^v//' | cut -d. -f1)
|
||||||
|
NODE_MIN=$(node -v | sed 's/^v//' | cut -d. -f2)
|
||||||
|
if [ "$NODE_MAJ" -lt 20 ] || { [ "$NODE_MAJ" -eq 20 ] && [ "$NODE_MIN" -lt 19 ]; }; then
|
||||||
|
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
||||||
|
apt-get install -y nodejs
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
||||||
|
apt-get install -y nodejs
|
||||||
|
fi
|
||||||
|
|
||||||
|
# .env anlegen & APP_KEY
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && cp -n .env.example .env || true"
|
||||||
|
if ! grep -q '^APP_KEY=' "${APP_DIR}/.env"; then echo "APP_KEY=" >> "${APP_DIR}/.env"; fi
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan key:generate --force || true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ===== .env füllen =====
|
||||||
|
ENV_FILE="${APP_DIR}/.env"
|
||||||
|
upsert_env () {
|
||||||
|
local key="$1" val="$2"
|
||||||
|
local esc_key esc_val
|
||||||
|
esc_key="$(printf '%s' "$key" | sed -e 's/[.[\*^$(){}+?|/]/\\&/g')"
|
||||||
|
esc_val="$(printf '%s' "$val" | sed -e 's/[&/]/\\&/g')"
|
||||||
|
if grep -qE "^[#[:space:]]*${esc_key}=" "$ENV_FILE"; then
|
||||||
|
sed -Ei "s|^[#[:space:]]*${esc_key}=.*|${key}=${esc_val}|g" "$ENV_FILE"
|
||||||
|
else
|
||||||
|
printf '%s=%s\n' "$key" "$val" >> "$ENV_FILE"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ -s "${CERT}" ] && [ -s "${KEY}" ]; then
|
||||||
|
upsert_env APP_URL "\"https://\${APP_HOST}\""
|
||||||
|
else
|
||||||
|
upsert_env APP_URL "\"http://\${APP_HOST}\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
upsert_env APP_HOST "${SERVER_IP}"
|
||||||
|
upsert_env APP_ADMIN_USER "${ADMIN_USER}"
|
||||||
|
upsert_env APP_ADMIN_EMAIL "${ADMIN_EMAIL}"
|
||||||
|
upsert_env APP_ADMIN_PASS "${ADMIN_PASS}"
|
||||||
|
upsert_env APP_NAME "${APP_NAME}"
|
||||||
|
upsert_env APP_ENV "${APP_ENV}"
|
||||||
|
upsert_env APP_DEBUG "${APP_DEBUG}"
|
||||||
|
|
||||||
|
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}"
|
||||||
|
|
||||||
|
# -------- WICHTIG: Cache-Store auf REDIS setzen (verhindert DB-Tabellenfehler) --------
|
||||||
|
upsert_env CACHE_SETTINGS_STORE "redis"
|
||||||
|
upsert_env CACHE_STORE "redis" # <- HIER geändert (vorher: database)
|
||||||
|
upsert_env CACHE_DRIVER "redis"
|
||||||
|
upsert_env CACHE_PREFIX "${APP_USER}_cache"
|
||||||
|
|
||||||
|
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}"
|
||||||
|
upsert_env REDIS_DB "0"
|
||||||
|
upsert_env REDIS_CACHE_DB "1"
|
||||||
|
upsert_env REDIS_CACHE_CONNECTION "cache"
|
||||||
|
upsert_env REDIS_CACHE_LOCK_CONNECTION "default"
|
||||||
|
|
||||||
|
# Reverb / Vite
|
||||||
|
upsert_env BROADCAST_DRIVER "reverb"
|
||||||
|
upsert_env QUEUE_CONNECTION "redis"
|
||||||
|
upsert_env REVERB_APP_ID "${APP_USER}"
|
||||||
|
upsert_env REVERB_APP_KEY "${APP_USER}-yhp47tbt1aebhr1fgvgj"
|
||||||
|
upsert_env REVERB_APP_SECRET "${APP_USER}-ulrdt9agwzkqwqsunbnb"
|
||||||
|
upsert_env REVERB_HOST "127.0.0.1"
|
||||||
|
upsert_env REVERB_PORT "443"
|
||||||
|
upsert_env REVERB_SCHEME "https"
|
||||||
|
upsert_env REVERB_PATH "/ws"
|
||||||
|
upsert_env VITE_REVERB_APP_KEY "\${REVERB_APP_KEY}"
|
||||||
|
upsert_env VITE_REVERB_PORT "\${REVERB_PORT}"
|
||||||
|
upsert_env VITE_REVERB_SCHEME "\${REVERB_SCHEME}"
|
||||||
|
upsert_env VITE_REVERB_PATH "\${REVERB_PATH}"
|
||||||
|
|
||||||
|
if [ "${DEV_MODE}" = "1" ]; then
|
||||||
|
sed -i '/^# --- MailWolt DEV/,/^# --- \/MailWolt DEV/d' "${ENV_FILE}"
|
||||||
|
cat >> "${ENV_FILE}" <<CONF
|
||||||
|
# --- MailWolt DEV ---
|
||||||
|
VITE_DEV_HOST=127.0.0.1
|
||||||
|
VITE_DEV_PORT=5173
|
||||||
|
VITE_HMR_PROTOCOL=wss
|
||||||
|
VITE_HMR_CLIENT_PORT=443
|
||||||
|
VITE_HMR_HOST=${APP_HOST}
|
||||||
|
VITE_DEV_ORIGIN=${APP_URL}
|
||||||
|
# --- /MailWolt DEV ---
|
||||||
|
CONF
|
||||||
|
cat > "${APP_DIR}/vite.config.js" <<'JS'
|
||||||
|
import { defineConfig, loadEnv } from 'vite'
|
||||||
|
import laravel from 'laravel-vite-plugin'
|
||||||
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
export default ({ mode }) => {
|
||||||
|
const env = loadEnv(mode, process.cwd(), '')
|
||||||
|
const host = env.VITE_DEV_HOST || '127.0.0.1'
|
||||||
|
const port = Number(env.VITE_DEV_PORT || 5173)
|
||||||
|
const origin = env.VITE_DEV_ORIGIN || env.APP_URL || 'https://localhost'
|
||||||
|
const hmrHost = env.VITE_HMR_HOST || (new URL(origin)).hostname
|
||||||
|
return defineConfig({
|
||||||
|
plugins: [laravel({ input: ['resources/css/app.css','resources/js/app.js'], refresh: true }), tailwindcss()],
|
||||||
|
server: { host, port, https:false, strictPort:true,
|
||||||
|
hmr:{ protocol: env.VITE_HMR_PROTOCOL || 'wss', host: hmrHost, clientPort:Number(env.VITE_HMR_CLIENT_PORT||443) },
|
||||||
|
origin
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
JS
|
||||||
|
chown "${APP_USER}:${APP_GROUP}" "${APP_DIR}/vite.config.js"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ===== Frontend Build =====
|
||||||
|
if [ -f "${APP_DIR}/package.json" ]; then
|
||||||
|
log "Frontend Build…"
|
||||||
|
if [ -f "${APP_DIR}/package-lock.json" ] || [ -f "${APP_DIR}/npm-shrinkwrap.json" ]; then
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm ci --no-audit --no-fund || npm install"
|
||||||
|
else
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm install"
|
||||||
|
fi
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm run build || true"
|
||||||
|
rm -f ${APP_DIR}/bootstrap/cache/*.php
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ===== Rechte / PHP-FPM =====
|
||||||
|
APP_PW="${APP_PW:-changeme}"
|
||||||
|
if ! id -u "$APP_USER" >/dev/null 2>&1; then
|
||||||
|
adduser --disabled-password --gecos "" "$APP_USER"
|
||||||
|
echo "${APP_USER}:${APP_PW}" | chpasswd
|
||||||
|
fi
|
||||||
|
usermod -a -G "$APP_GROUP" "$APP_USER"
|
||||||
|
|
||||||
|
# Sichert, dass alle nötigen Ordner existieren (idempotent)
|
||||||
|
install -d -m 0775 "${APP_DIR}/storage" \
|
||||||
|
"${APP_DIR}/storage/framework" \
|
||||||
|
"${APP_DIR}/storage/framework/cache" \
|
||||||
|
"${APP_DIR}/storage/framework/cache/data" \
|
||||||
|
"${APP_DIR}/storage/framework/sessions" \
|
||||||
|
"${APP_DIR}/storage/framework/views" \
|
||||||
|
"${APP_DIR}/bootstrap/cache"
|
||||||
|
|
||||||
|
# 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 {} \;
|
||||||
|
|
||||||
|
[ -f "$APP_DIR/artisan" ] && chmod 755 "$APP_DIR/artisan"
|
||||||
|
[ -d "$APP_DIR/vendor/bin" ] && chmod -R 755 "$APP_DIR/vendor/bin"
|
||||||
|
[ -d "$APP_DIR/node_modules/.bin" ] && chmod -R 755 "$APP_DIR/node_modules/.bin"
|
||||||
|
[ -f "$APP_DIR/node_modules/vite/bin/vite.js" ] && chmod 755 "$APP_DIR/node_modules/vite/bin/vite.js"
|
||||||
|
find "$APP_DIR" -type f -name "*.sh" -exec chmod 755 {} \;
|
||||||
|
|
||||||
|
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
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "npm config set umask 0002" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
PHPV=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;')
|
||||||
|
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
|
||||||
|
|
||||||
|
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 "${GREEN}[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
|
||||||
|
|
||||||
|
# ===== Reverb systemd =====
|
||||||
|
cat > /etc/systemd/system/mailwolt-ws.service <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=MailWolt WebSocket Backend
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
Environment=NODE_ENV=production WS_PORT=8080
|
||||||
|
User=${APP_USER}
|
||||||
|
Group=${APP_GROUP}
|
||||||
|
WorkingDirectory=${APP_DIR}
|
||||||
|
ExecStart=/usr/bin/php artisan reverb:start --host=127.0.0.1 --port=8080 --no-interaction
|
||||||
|
Restart=always
|
||||||
|
RestartSec=2
|
||||||
|
StandardOutput=append:/var/log/mailwolt-ws.log
|
||||||
|
StandardError=append:/var/log/mailwolt-ws.log
|
||||||
|
KillSignal=SIGINT
|
||||||
|
TimeoutStopSec=15
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
chown root:root /etc/systemd/system/mailwolt-ws.service
|
||||||
|
chmod 644 /etc/systemd/system/mailwolt-ws.service
|
||||||
|
|
||||||
|
touch /var/log/mailwolt-ws.log
|
||||||
|
chown ${APP_USER}:${APP_GROUP} /var/log/mailwolt-ws.log
|
||||||
|
chmod 664 /var/log/mailwolt-ws.log
|
||||||
|
|
||||||
|
install -d -m 775 -o "${APP_USER}" -g "${APP_GROUP}" \
|
||||||
|
"${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache"
|
||||||
|
|
||||||
|
# ---- Laravel-Caches als APP_USER aufräumen (funktioniert jetzt ohne DB-Cache-Tabelle) ----
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan optimize:clear && php artisan config:cache"
|
||||||
|
|
||||||
|
# HINWEIS: Wenn du unbedingt DATABASE als Cache-Store willst, dann vor obiger Zeile:
|
||||||
|
# sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan cache:table && php artisan migrate --force"
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
if sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan list --no-ansi 2>/dev/null | grep -qE '(^| )reverb:start( |$)'"; then
|
||||||
|
systemctl enable --now mailwolt-ws
|
||||||
|
else
|
||||||
|
systemctl disable --now mailwolt-ws >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ===== Monit =====
|
||||||
|
log "Monit konfigurieren & starten…"
|
||||||
|
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 /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
|
||||||
|
if failed port 110 type tcp then restart
|
||||||
|
if failed port 995 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-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 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
|
||||||
|
|
||||||
|
check process mailwolt-ws matching "reverb:start"
|
||||||
|
start program = "/bin/systemctl start mailwolt-ws"
|
||||||
|
stop program = "/bin/systemctl stop mailwolt-ws"
|
||||||
|
if failed host 127.0.0.1 port 8080 type tcp for 2 cycles then restart
|
||||||
|
if 5 restarts within 5 cycles then timeout
|
||||||
|
EOF
|
||||||
|
chmod 600 /etc/monit/monitrc
|
||||||
|
monit -t && systemctl enable --now monit
|
||||||
|
monit reload
|
||||||
|
monit summary || true
|
||||||
|
|
||||||
|
# ===== Healthchecks =====
|
||||||
|
GREEN="\033[1;32m"; RED="\033[1;31m"; GREY="\033[0;90m"; NC="\033[0m"
|
||||||
|
ok(){ echo -e " [${GREEN}OK${NC}]"; }
|
||||||
|
fail(){ echo -e " [${RED}FAIL${NC}]"; }
|
||||||
|
|
||||||
|
echo "[+] Quick-Healthchecks…"
|
||||||
|
printf " • MariaDB … " ; mysqladmin ping --silent >/dev/null 2>&1 && ok || fail
|
||||||
|
printf " • Redis … " ; if command -v redis-cli >/dev/null 2>&1; then
|
||||||
|
if [ -n "${REDIS_PASS:-}" ] && [ "${REDIS_PASS}" != "null" ]; then
|
||||||
|
redis-cli -a "${REDIS_PASS}" ping 2>/dev/null | grep -q PONG && ok || fail
|
||||||
|
else
|
||||||
|
redis-cli ping 2>/dev/null | grep -q PONG && ok || fail
|
||||||
|
fi
|
||||||
|
else fail; fi
|
||||||
|
printf " • PHP-FPM … " ; if [[ "$PHP_FPM_SOCK" == 127.0.0.1:9000 ]]; then ss -ltn | grep -q ":9000 " && ok || fail; else [ -S "$PHP_FPM_SOCK" ] && ok || fail; fi
|
||||||
|
printf " • App … " ; if command -v curl >/dev/null 2>&1; then
|
||||||
|
if [ -s "${CERT}" ] && [ -s "${KEY}" ]; then curl -skI "https://127.0.0.1" >/dev/null 2>&1 && ok || fail; else curl -sI "http://127.0.0.1" >/dev/null 2>&1 && ok || fail; fi
|
||||||
|
else echo -e " ${GREY}(curl fehlt)${NC}"; fi
|
||||||
|
check_port(){ local label="$1" cmd="$2"; printf " • %-5s … " "$label"; timeout 8s bash -lc "$cmd" >/dev/null 2>&1 && ok || fail; }
|
||||||
|
check_port "25" 'printf "QUIT\r\n" | nc -w 3 127.0.0.1 25'
|
||||||
|
check_port "465" 'printf "QUIT\r\n" | openssl s_client -connect 127.0.0.1:465 -quiet -ign_eof'
|
||||||
|
check_port "587" 'printf "EHLO x\r\nSTARTTLS\r\nQUIT\r\n" | openssl s_client -starttls smtp -connect 127.0.0.1:587 -quiet -ign_eof'
|
||||||
|
check_port "110" 'printf "QUIT\r\n" | nc -w 3 127.0.0.1 110'
|
||||||
|
check_port "995" 'printf "QUIT\r\n" | openssl s_client -connect 127.0.0.1:995 -quiet -ign_eof'
|
||||||
|
check_port "143" 'printf ". LOGOUT\r\n" | nc -w 3 127.0.0.1 143'
|
||||||
|
check_port "993" 'printf ". LOGOUT\r\n" | openssl s_client -connect 127.0.0.1:993 -quiet -ign_eof'
|
||||||
|
|
||||||
|
print_bootstrap_summary "$SERVER_IP" "$ADMIN_USER" "$ADMIN_PASS"
|
||||||
|
|
||||||
|
# ===== MOTD =====
|
||||||
|
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 '?')"
|
||||||
|
mhz="$(LC_ALL=C lscpu 2>/dev/null | awk -F: '/MHz/{gsub(/ /,"",$2); printf("%.0f MHz",$2); exit}')"
|
||||||
|
[ -z "$mhz" ] && mhz="$(awk -F: '/cpu MHz/{printf("%.0f MHz",$2); exit}' /proc/cpuinfo 2>/dev/null)"
|
||||||
|
load="$(awk '{print $1" / "$2" / "$3}' /proc/loadavg 2>/dev/null)"
|
||||||
|
mem_total="$(awk '/MemTotal/{printf "%.2f GB",$2/1024/1024}' /proc/meminfo)"
|
||||||
|
mem_free="$(awk '/MemAvailable/{printf "%.2f GB",$2/1024/1024}' /proc/meminfo)"
|
||||||
|
svc_status(){ 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}(external:${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}Core(s) :${NC} %s core(s) at ${CY}%s${NC}\n" "$cores" "${mhz:-?}"
|
||||||
|
printf "${GY}Load :${NC} %s (1 / 5 / 15)\n" "${load:-?}"
|
||||||
|
printf "${GY}Memory :${NC} ${RD}%s${NC} ${GY}(free)${NC} / ${CY}%s${NC} ${GY}(total)${NC}\n" "${mem_free:-?}" "${mem_total:-?}"
|
||||||
|
echo
|
||||||
|
printf "${GY}Services :${NC} postfix: $(svc_status postfix) dovecot: $(svc_status dovecot) nginx: $(svc_status nginx) mariadb: $(svc_status mariadb) redis: $(svc_status 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
|
||||||
|
[ -f /etc/update-motd.d/80-livepatch ] && chmod -x /etc/update-motd.d/80-livepatch || 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
|
||||||
|
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
if [ -r /etc/mailwolt/installer.env ]; then
|
||||||
|
. /etc/mailwolt/installer.env
|
||||||
|
fi
|
||||||
|
|
||||||
|
REDIS_PASS="${REDIS_PASS:-}"
|
||||||
|
|
||||||
|
SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
find "$SCRIPTS_DIR/.." -type f -name "*.sh" -exec sed -i 's/\r$//' {} \; || true
|
||||||
|
|
||||||
|
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 opendmarc clamav \
|
||||||
|
clamav-daemon 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 fail2ban ca-certificates \
|
||||||
|
rsyslog sudo openssl monit acl netcat-openbsd jq sqlite3
|
||||||
|
|
||||||
|
# <<< Apache konsequent entfernen >>>
|
||||||
|
systemctl disable --now apache2 >/dev/null 2>&1 || true
|
||||||
|
apt-get -y purge 'apache2*' >/dev/null 2>&1 || true
|
||||||
|
apt-get -y autoremove >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
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"
|
||||||
|
# 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"
|
||||||
|
|
||||||
|
# --- FIX: Gruppen und Berechtigungen für Maildir und Dovecot-Zugriff ---
|
||||||
|
# vmail soll primär der Gruppe "mail" angehören, zusätzlich dovecot
|
||||||
|
usermod -g mail -a -G dovecot vmail || true
|
||||||
|
|
||||||
|
# App-User in relevante Gruppen
|
||||||
|
usermod -a -G "$APP_GROUP" "$APP_USER" || true
|
||||||
|
usermod -a -G mail,dovecot "$APP_USER" || true
|
||||||
|
|
||||||
|
# Maildir-Baum für Gruppe mail lesbar
|
||||||
|
chgrp -R mail /var/mail/vhosts || true
|
||||||
|
chmod -R g+rx /var/mail/vhosts || true
|
||||||
|
|
||||||
|
# ACLs setzen, damit neue Verzeichnisse automatisch passende Rechte bekommen
|
||||||
|
setfacl -R -m g:mail:rx /var/mail/vhosts || true
|
||||||
|
setfacl -dR -m g:mail:rx /var/mail/vhosts || true
|
||||||
|
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"
|
||||||
|
|
||||||
|
SUDOERS_DKIM="/etc/sudoers.d/mailwolt-dkim"
|
||||||
|
cat > "${SUDOERS_DKIM}" <<'EOF'
|
||||||
|
Defaults!/usr/local/sbin/mailwolt-install-dkim !requiretty
|
||||||
|
Defaults!/usr/local/sbin/mailwolt-remove-dkim !requiretty
|
||||||
|
Defaults!/usr/bin/systemctl !requiretty
|
||||||
|
Defaults!/usr/bin/test !requiretty
|
||||||
|
|
||||||
|
www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-install-dkim *
|
||||||
|
www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-remove-dkim *
|
||||||
|
www-data ALL=(root) NOPASSWD: /usr/bin/systemctl reload opendkim
|
||||||
|
www-data ALL=(root) NOPASSWD: /usr/bin/test *
|
||||||
|
|
||||||
|
mailwolt ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-install-dkim *
|
||||||
|
mailwolt ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-remove-dkim *
|
||||||
|
mailwolt ALL=(root) NOPASSWD: /usr/bin/systemctl reload opendkim
|
||||||
|
mailwolt ALL=(root) NOPASSWD: /usr/bin/test *
|
||||||
|
EOF
|
||||||
|
chown root:root "${SUDOERS_DKIM}"
|
||||||
|
chmod 440 "${SUDOERS_DKIM}"
|
||||||
|
|
||||||
|
if ! visudo -c -f "${SUDOERS_DKIM}" >/dev/null 2>&1; then
|
||||||
|
echo "[!] Ungültiger sudoers-Eintrag in ${SUDOERS_DKIM} – entferne Datei."
|
||||||
|
rm -f "${SUDOERS_DKIM}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
SUDOERS_DOVEADM="/etc/sudoers.d/mailwolt-doveadm"
|
||||||
|
cat > "${SUDOERS_DOVEADM}" <<'EOF'
|
||||||
|
Cmnd_Alias MW_DOVEADM_STATUS = /usr/bin/doveadm -f tab mailbox status -u * messages INBOX, \
|
||||||
|
/usr/bin/doveadm mailbox status -u * messages INBOX
|
||||||
|
www-data ALL=(vmail) NOPASSWD: MW_DOVEADM_STATUS
|
||||||
|
mailwolt ALL=(vmail) NOPASSWD: MW_DOVEADM_STATUS
|
||||||
|
EOF
|
||||||
|
chown root:root "${SUDOERS_DOVEADM}"
|
||||||
|
chmod 440 "${SUDOERS_DOVEADM}"
|
||||||
|
visudo -c -f "${SUDOERS_DOVEADM}" || rm -f "${SUDOERS_DOVEADM}"
|
||||||
|
|
||||||
|
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 …"
|
||||||
|
if [[ -z "${REDIS_PASS:-}" || "${REDIS_PASS}" == "changeme" ]]; then
|
||||||
|
REDIS_PASS="$(openssl rand -hex 16)"
|
||||||
|
export REDIS_PASS
|
||||||
|
log "Neues Redis-Passwort generiert."
|
||||||
|
fi
|
||||||
|
# Aktiven Redis-Config-Pfad aus systemd holen (Fallback: Standard)
|
||||||
|
REDIS_CONF="$(systemctl show -p ExecStart redis-server \
|
||||||
|
| sed -n 's/^ExecStart=.*redis-server[[:space:]]\+\([^[:space:]]\+\).*/\1/p')"
|
||||||
|
REDIS_CONF="${REDIS_CONF:-/etc/redis/redis.conf}"
|
||||||
|
|
||||||
|
# Bind + protected-mode hart setzen
|
||||||
|
sed -i 's/^[[:space:]]*#\?[[:space:]]*bind .*/bind 127.0.0.1/' "$REDIS_CONF"
|
||||||
|
sed -i 's/^[[:space:]]*#\?[[:space:]]*protected-mode .*/protected-mode yes/' "$REDIS_CONF"
|
||||||
|
|
||||||
|
# Vorherige requirepass-Zeilen entfernen (kommentiert/unkommentiert), dann neu schreiben
|
||||||
|
sed -i '/^[[:space:]]*#\?[[:space:]]*requirepass[[:space:]]\+/d' "$REDIS_CONF"
|
||||||
|
printf '\nrequirepass %s\n' "${REDIS_PASS}" >> "$REDIS_CONF"
|
||||||
|
|
||||||
|
# Dienst aktivieren & neu starten
|
||||||
|
systemctl enable --now redis-server
|
||||||
|
systemctl restart redis-server || true
|
||||||
|
|
||||||
|
# Sanity-Check (kein harter Exit, nur Log)
|
||||||
|
if redis-cli -a "${REDIS_PASS}" ping 2>/dev/null | grep -q PONG; then
|
||||||
|
log "Redis mit Passwort OK."
|
||||||
|
else
|
||||||
|
warn "Redis PING mit Passwort fehlgeschlagen – bitte /etc/redis/redis.conf prüfen."
|
||||||
|
fi
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
#!/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" <<CFG
|
||||||
|
[req]
|
||||||
|
default_bits=2048
|
||||||
|
prompt=no
|
||||||
|
default_md=sha256
|
||||||
|
req_extensions=req_ext
|
||||||
|
distinguished_name=dn
|
||||||
|
[dn]
|
||||||
|
CN=${SERVER_PUBLIC_IPV4}
|
||||||
|
O=${APP_NAME}
|
||||||
|
C=DE
|
||||||
|
[req_ext]
|
||||||
|
subjectAltName=@alt_names
|
||||||
|
[alt_names]
|
||||||
|
IP.1=${SERVER_PUBLIC_IPV4}
|
||||||
|
CFG
|
||||||
|
openssl req -x509 -newkey rsa:2048 -days 825 -nodes -keyout "$KEY" -out "$CERT" -config "$OSSL_CFG"
|
||||||
|
chgrp www-data "$CERT" "$KEY" || true
|
||||||
|
chmod 640 "$KEY" "$CERT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
install -d -m 0755 "$UI_SSL_DIR" "$WEBMAIL_SSL_DIR" "$MAIL_SSL_DIR"
|
||||||
|
ln -sf "$CERT" "$UI_CERT"; ln -sf "$KEY" "$UI_KEY"
|
||||||
|
ln -sf "$CERT" "$WEBMAIL_CERT";ln -sf "$KEY" "$WEBMAIL_KEY"
|
||||||
|
ln -sf "$CERT" "$MAIL_CERT"; ln -sf "$KEY" "$MAIL_KEY"
|
||||||
|
|
||||||
|
# --- Mail-Zertifikate: Rechte für Postfix & Dovecot -------------------------
|
||||||
|
# WICHTIG: Rechte am *Target* (KEY/CERT im $CERT_DIR) setzen, nicht an den Symlinks.
|
||||||
|
if [[ -f "$KEY" && -f "$CERT" ]]; then
|
||||||
|
echo "[+] Setze Berechtigungen für Mail-Zertifikate …"
|
||||||
|
# Key: nur root + Gruppe lesen. Gruppe → postfix
|
||||||
|
chgrp postfix "$KEY" || true
|
||||||
|
chmod 640 "$KEY" || true
|
||||||
|
# Dovecot zusätzlich Leserechte via ACL
|
||||||
|
setfacl -m u:dovecot:r "$KEY" || true
|
||||||
|
# Zertifikat darf weltweit lesbar sein
|
||||||
|
chmod 644 "$CERT" || true
|
||||||
|
else
|
||||||
|
echo "[!] Zertifikatsdateien fehlen: $KEY oder $CERT" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Optional: kurze Info, wohin verlinkt wurde
|
||||||
|
echo "[i] Mail TLS: $MAIL_CERT -> $CERT ; $MAIL_KEY -> $KEY"
|
||||||
|
|
@ -0,0 +1,588 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
log "Let's Encrypt Deploy-Hooks und Wrapper anlegen …"
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# 2) POSIX-kompatibler Deploy-Wrapper (von Certbot aufgerufen)
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
cat >/usr/local/sbin/mailwolt-deploy.sh <<'WRAP'
|
||||||
|
#!/bin/sh
|
||||||
|
# POSIX-safe Certbot deploy-hook (ohne bashisms)
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
# Installer-ENV laden (liefert UI_HOST/WEBMAIL_HOST/MAIL_HOSTNAME etc.)
|
||||||
|
if [ -r /etc/mailwolt/installer.env ]; then
|
||||||
|
. /etc/mailwolt/installer.env
|
||||||
|
fi
|
||||||
|
|
||||||
|
UI_HOST="${UI_HOST:-}"
|
||||||
|
WEBMAIL_HOST="${WEBMAIL_HOST:-}"
|
||||||
|
MAIL_HOSTNAME="${MAIL_HOSTNAME:-}"
|
||||||
|
ACME_BASE="/etc/letsencrypt/live"
|
||||||
|
|
||||||
|
copy_cert() {
|
||||||
|
le_base="$1" # z.B. /etc/letsencrypt/live/ui.example.com
|
||||||
|
target_dir="$2" # z.B. /etc/ssl/ui
|
||||||
|
|
||||||
|
cert="${le_base}/fullchain.pem"
|
||||||
|
key="${le_base}/privkey.pem"
|
||||||
|
|
||||||
|
[ -s "$cert" ] || { echo "[deploy] missing $cert"; return 1; }
|
||||||
|
[ -s "$key" ] || { echo "[deploy] missing $key"; return 1; }
|
||||||
|
|
||||||
|
mkdir -p "$target_dir"
|
||||||
|
|
||||||
|
# echte Dateien (keine Symlinks), feste Rechte
|
||||||
|
install -m 0644 "$cert" "${target_dir}/fullchain.pem"
|
||||||
|
install -m 0600 "$key" "${target_dir}/privkey.pem"
|
||||||
|
|
||||||
|
echo "[+] Copied ${target_dir}/fullchain.pem und privkey.pem ← ${le_base}"
|
||||||
|
}
|
||||||
|
|
||||||
|
reload_services() {
|
||||||
|
kind="$1" # ui | mail
|
||||||
|
if command -v systemctl >/dev/null 2>&1; then
|
||||||
|
if [ "$kind" = "mail" ]; then
|
||||||
|
systemctl reload postfix 2>/dev/null || true
|
||||||
|
systemctl reload dovecot 2>/dev/null || true
|
||||||
|
else
|
||||||
|
systemctl reload nginx 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Certbot-Kontext
|
||||||
|
LINEAGE="${RENEWED_LINEAGE:-}"
|
||||||
|
HOST=""
|
||||||
|
if [ -n "$LINEAGE" ]; then
|
||||||
|
HOST="$(basename "$LINEAGE")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
did_any=0
|
||||||
|
|
||||||
|
maybe_copy_for_host() {
|
||||||
|
host="$1"
|
||||||
|
dir="$2"
|
||||||
|
[ -n "$host" ] || return 0
|
||||||
|
|
||||||
|
# Fall A: Certbot liefert RENEWED_DOMAINS (Space-getrennt)
|
||||||
|
if [ -n "${RENEWED_DOMAINS:-}" ]; then
|
||||||
|
case " ${RENEWED_DOMAINS} " in
|
||||||
|
*" ${host} "*) copy_cert "${ACME_BASE}/${host}" "${dir}" && did_any=1 ;;
|
||||||
|
esac
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fall B: Erst-issue / kein RENEWED_DOMAINS → über LINEAGE matchen
|
||||||
|
if [ -n "$HOST" ] && [ "$HOST" = "$host" ]; then
|
||||||
|
copy_cert "${ACME_BASE}/${host}" "${dir}" && did_any=1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Gezieltes Kopieren
|
||||||
|
maybe_copy_for_host "$UI_HOST" "/etc/ssl/ui"
|
||||||
|
maybe_copy_for_host "$WEBMAIL_HOST" "/etc/ssl/webmail"
|
||||||
|
maybe_copy_for_host "$MAIL_HOSTNAME" "/etc/ssl/mail"
|
||||||
|
|
||||||
|
# Fallback (Erstlauf): kopiere vorhandene Lineages
|
||||||
|
if [ "$did_any" -eq 0 ]; then
|
||||||
|
[ -n "$UI_HOST" ] && [ -d "${ACME_BASE}/${UI_HOST}" ] && copy_cert "${ACME_BASE}/${UI_HOST}" "/etc/ssl/ui"
|
||||||
|
[ -n "$WEBMAIL_HOST" ] && [ -d "${ACME_BASE}/${WEBMAIL_HOST}" ] && copy_cert "${ACME_BASE}/${WEBMAIL_HOST}" "/etc/ssl/webmail"
|
||||||
|
[ -n "$MAIL_HOSTNAME" ] && [ -d "${ACME_BASE}/${MAIL_HOSTNAME}" ] && copy_cert "${ACME_BASE}/${MAIL_HOSTNAME}" "/etc/ssl/mail"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# TLSA-Refresh (tolerant falls App noch nicht ready)
|
||||||
|
if command -v php >/dev/null 2>&1 && [ -f /var/www/mailwolt/artisan ]; then
|
||||||
|
(cd /var/www/mailwolt && php artisan dns:tlsa:refresh) || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Services neu laden
|
||||||
|
if [ -n "$HOST" ]; then
|
||||||
|
if [ -n "$MAIL_HOSTNAME" ] && [ "$HOST" = "$MAIL_HOSTNAME" ]; then
|
||||||
|
reload_services mail
|
||||||
|
else
|
||||||
|
reload_services ui
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
reload_services ui
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
WRAP
|
||||||
|
chmod +x /usr/local/sbin/mailwolt-deploy.sh
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# 3) Certbot deploy-hook, der den Wrapper aufruft
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
install -d -m 0755 /etc/letsencrypt/renewal-hooks/deploy
|
||||||
|
cat >/etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-certs.sh <<'HOOK'
|
||||||
|
#!/bin/sh
|
||||||
|
exec /usr/local/sbin/mailwolt-deploy.sh
|
||||||
|
HOOK
|
||||||
|
chmod +x /etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-certs.sh
|
||||||
|
|
||||||
|
|
||||||
|
log "[✓] MailWolt Deploy-Hook eingerichtet"
|
||||||
|
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#set -euo pipefail
|
||||||
|
#source ./lib.sh
|
||||||
|
#
|
||||||
|
## Persistente Installer-Variablen (werden vom Wrapper gelesen)
|
||||||
|
#install -d -m 0755 /etc/mailwolt
|
||||||
|
#cat >/etc/mailwolt/installer.env <<EOF
|
||||||
|
#UI_HOST=${UI_HOST}
|
||||||
|
#WEBMAIL_HOST=${WEBMAIL_HOST}
|
||||||
|
#MAIL_HOSTNAME=${MAIL_HOSTNAME}
|
||||||
|
#BASE_DOMAIN=${BASE_DOMAIN}
|
||||||
|
#LE_EMAIL=${LE_EMAIL:-admin@${BASE_DOMAIN}}
|
||||||
|
#SYSMAIL_SUB="${SYSMAIL_SUB}"
|
||||||
|
#SYSMAIL_DOMAIN="${SYSMAIL_DOMAIN}"
|
||||||
|
#DKIM_ENABLE="${DKIM_ENABLE}"
|
||||||
|
#DKIM_SELECTOR="${DKIM_SELECTOR}"
|
||||||
|
#DKIM_GENERATE="${DKIM_GENERATE}"
|
||||||
|
#APP_ENV=${APP_ENV:-production}
|
||||||
|
#EOF
|
||||||
|
#
|
||||||
|
#log "Let's Encrypt Deploy-Hooks und Wrapper anlegen …"
|
||||||
|
#
|
||||||
|
## 1) Wrapper, den Certbot bei Issue/Renew aufruft
|
||||||
|
#cat >/usr/local/sbin/mw-deploy.sh <<'WRAP'
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#set -euo pipefail
|
||||||
|
#
|
||||||
|
## Installer-Variablen laden
|
||||||
|
#set +u
|
||||||
|
#[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
|
||||||
|
#set -u
|
||||||
|
#
|
||||||
|
#UI_HOST="${UI_HOST:-}"
|
||||||
|
#WEBMAIL_HOST="${WEBMAIL_HOST:-}"
|
||||||
|
#MAIL_HOSTNAME="${MAIL_HOSTNAME:-}"
|
||||||
|
#
|
||||||
|
## --- Kopieren statt Symlinks (damit Laravel lesen kann) ---------------------
|
||||||
|
#copy_cert() {
|
||||||
|
# local le_base="$1" target_dir="$2"
|
||||||
|
# local cert="${le_base}/fullchain.pem"
|
||||||
|
# local key="${le_base}/privkey.pem"
|
||||||
|
#
|
||||||
|
# [[ -s "$cert" && -s "$key" ]] || return 0
|
||||||
|
#
|
||||||
|
# install -d -m 0755 "$target_dir"
|
||||||
|
#
|
||||||
|
# # Vorhandene Symlinks entfernen, sonst kopierst du in die LE-Datei hinein
|
||||||
|
# [ -L "${target_dir}/fullchain.pem" ] && rm -f "${target_dir}/fullchain.pem"
|
||||||
|
# [ -L "${target_dir}/privkey.pem" ] && rm -f "${target_dir}/privkey.pem"
|
||||||
|
#
|
||||||
|
# # Echte Dateien ablegen
|
||||||
|
# install -m 0644 "$cert" "${target_dir}/fullchain.pem"
|
||||||
|
# install -m 0600 "$key" "${target_dir}/privkey.pem"
|
||||||
|
#
|
||||||
|
# echo "[+] Copied ${target_dir}/fullchain.pem und privkey.pem ← ${le_base}"
|
||||||
|
#}
|
||||||
|
#
|
||||||
|
## Nur Domains bearbeiten, die in diesem Lauf betroffen sind.
|
||||||
|
## Bei manchen Distros ist RENEWED_DOMAINS auf Erst-issue leer -> Fallback nutzen.
|
||||||
|
#RDOMS=" ${RENEWED_DOMAINS:-} "
|
||||||
|
#did_any=0
|
||||||
|
#
|
||||||
|
#maybe_copy_for() {
|
||||||
|
# local host="$1" dir="$2"
|
||||||
|
# [[ -z "$host" ]] && return 0
|
||||||
|
# if [[ "$RDOMS" == *" ${host} "* ]]; then
|
||||||
|
# copy_cert "/etc/letsencrypt/live/${host}" "${dir}"
|
||||||
|
# did_any=1
|
||||||
|
# fi
|
||||||
|
#}
|
||||||
|
#
|
||||||
|
## 1) Normalfall: nur die vom Certbot gemeldeten Hosts kopieren
|
||||||
|
#maybe_copy_for "$UI_HOST" "/etc/ssl/ui"
|
||||||
|
#maybe_copy_for "$WEBMAIL_HOST" "/etc/ssl/webmail"
|
||||||
|
#maybe_copy_for "$MAIL_HOSTNAME" "/etc/ssl/mail"
|
||||||
|
#
|
||||||
|
## 2) Fallback: Beim Erstlauf/Edge-Cases alles kopieren, was bereits existiert
|
||||||
|
#if [[ "$did_any" -eq 0 ]]; then
|
||||||
|
# [[ -n "$UI_HOST" && -d "/etc/letsencrypt/live/${UI_HOST}" ]] && copy_cert "/etc/letsencrypt/live/${UI_HOST}" "/etc/ssl/ui"
|
||||||
|
# [[ -n "$WEBMAIL_HOST" && -d "/etc/letsencrypt/live/${WEBMAIL_HOST}" ]] && copy_cert "/etc/letsencrypt/live/${WEBMAIL_HOST}" "/etc/ssl/webmail"
|
||||||
|
# [[ -n "$MAIL_HOSTNAME" && -d "/etc/letsencrypt/live/${MAIL_HOSTNAME}"]] && copy_cert "/etc/letsencrypt/live/${MAIL_HOSTNAME}"/etc/ssl/mail
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
## Optional: TLSA via Laravel (tolerant, falls App noch nicht gebaut)
|
||||||
|
#if command -v php >/dev/null 2>&1 && [ -d /var/www/mailwolt ] && [ -f /var/www/mailwolt/artisan ]; then
|
||||||
|
# (cd /var/www/mailwolt && php artisan dns:tlsa:refresh) || true
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
## Nginx nur neu laden, wenn aktiv
|
||||||
|
#if systemctl is-active --quiet nginx; then
|
||||||
|
# systemctl reload nginx || true
|
||||||
|
#fi
|
||||||
|
#WRAP
|
||||||
|
#chmod +x /usr/local/sbin/mw-deploy.sh
|
||||||
|
#
|
||||||
|
## 2) Certbot-Deploy-Hook: ruft den Wrapper bei jeder erfolgreichen Ausstellung/Renew auf
|
||||||
|
#install -d -m 0755 /etc/letsencrypt/renewal-hooks/deploy
|
||||||
|
#cat >/etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-certs.sh <<'HOOK'
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#exec /usr/local/sbin/mw-deploy.sh
|
||||||
|
#HOOK
|
||||||
|
#chmod +x /etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-certs.sh
|
||||||
|
#
|
||||||
|
#log "[✓] MailWolt Deploy-Hook eingerichtet"
|
||||||
|
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#set -euo pipefail
|
||||||
|
#source ./lib.sh
|
||||||
|
#
|
||||||
|
## Persistente Installer-Variablen (werden vom Wrapper gelesen)
|
||||||
|
#install -d -m 0755 /etc/mailwolt
|
||||||
|
#cat >/etc/mailwolt/installer.env <<EOF
|
||||||
|
#UI_HOST=${UI_HOST}
|
||||||
|
#WEBMAIL_HOST=${WEBMAIL_HOST}
|
||||||
|
#MAIL_HOSTNAME=${MAIL_HOSTNAME}
|
||||||
|
#BASE_DOMAIN=${BASE_DOMAIN}
|
||||||
|
#LE_EMAIL=${LE_EMAIL:-admin@${BASE_DOMAIN}}
|
||||||
|
#APP_ENV=${APP_ENV:-production}
|
||||||
|
#EOF
|
||||||
|
#
|
||||||
|
#log "Let's Encrypt Deploy-Hooks und Wrapper anlegen …"
|
||||||
|
#
|
||||||
|
## 1) Wrapper, den Certbot bei Issue/Renew aufruft
|
||||||
|
#cat >/usr/local/sbin/mw-deploy.sh <<'WRAP'
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#set -euo pipefail
|
||||||
|
#
|
||||||
|
## Installer-Variablen laden
|
||||||
|
#set +u
|
||||||
|
#[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
|
||||||
|
#set -u
|
||||||
|
#
|
||||||
|
#UI_HOST="${UI_HOST:-}"
|
||||||
|
#WEBMAIL_HOST="${WEBMAIL_HOST:-}"
|
||||||
|
#MAIL_HOSTNAME="${MAIL_HOSTNAME:-}"
|
||||||
|
#
|
||||||
|
## --- Kopieren statt Symlinks (damit Laravel lesen kann) ---------------------
|
||||||
|
#copy_cert() {
|
||||||
|
# local le_base="$1" target_dir="$2"
|
||||||
|
# local cert="${le_base}/fullchain.pem"
|
||||||
|
# local key="${le_base}/privkey.pem"
|
||||||
|
#
|
||||||
|
# [[ -s "$cert" && -s "$key" ]] || return 0
|
||||||
|
#
|
||||||
|
# # Zielordner sicherstellen
|
||||||
|
# install -d -m 0755 "$target_dir"
|
||||||
|
#
|
||||||
|
# # Falls vorher Symlinks existieren → entfernen, sonst würde "install" das Ziel des Links überschreiben
|
||||||
|
# [ -L "${target_dir}/fullchain.pem" ] && rm -f "${target_dir}/fullchain.pem"
|
||||||
|
# [ -L "${target_dir}/privkey.pem" ] && rm -f "${target_dir}/privkey.pem"
|
||||||
|
#
|
||||||
|
# # KOPIEREN mit sauberen Rechten (Chain world-readable, Key nur root)
|
||||||
|
# install -m 0644 "$cert" "${target_dir}/fullchain.pem"
|
||||||
|
# install -m 0600 "$key" "${target_dir}/privkey.pem"
|
||||||
|
#
|
||||||
|
# echo "[+] Copied ${target_dir}/fullchain.pem und privkey.pem ← ${le_base}"
|
||||||
|
#}
|
||||||
|
#
|
||||||
|
## Nur für Domains arbeiten, die in diesem Lauf betroffen sind
|
||||||
|
#RDOMS=" ${RENEWED_DOMAINS:-} "
|
||||||
|
#
|
||||||
|
## UI
|
||||||
|
#if [[ -n "$UI_HOST" && "$RDOMS" == *" ${UI_HOST} "* ]]; then
|
||||||
|
# copy_cert "/etc/letsencrypt/live/${UI_HOST}" "/etc/ssl/ui"
|
||||||
|
#fi
|
||||||
|
## Webmail
|
||||||
|
#if [[ -n "$WEBMAIL_HOST" && "$RDOMS" == *" ${WEBMAIL_HOST} "* ]]; then
|
||||||
|
# copy_cert "/etc/letsencrypt/live/${WEBMAIL_HOST}" "/etc/ssl/webmail"
|
||||||
|
#fi
|
||||||
|
## MX
|
||||||
|
#if [[ -n "$MAIL_HOSTNAME" && "$RDOMS" == *" ${MAIL_HOSTNAME} "* ]]; then
|
||||||
|
# copy_cert "/etc/letsencrypt/live/${MAIL_HOSTNAME}" "/etc/ssl/mail"
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
## Optional: TLSA via Laravel (still tolerant, falls App noch nicht gebaut)
|
||||||
|
#if command -v php >/dev/null 2>&1 && [ -d /var/www/mailwolt ] && [ -f /var/www/mailwolt/artisan ]; then
|
||||||
|
# (cd /var/www/mailwolt && php artisan dns:tlsa:refresh) || true
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
## Nginx nur neu laden, wenn aktiv
|
||||||
|
#if systemctl is-active --quiet nginx; then
|
||||||
|
# systemctl reload nginx || true
|
||||||
|
#fi
|
||||||
|
#WRAP
|
||||||
|
#chmod +x /usr/local/sbin/mw-deploy.sh
|
||||||
|
#
|
||||||
|
## 2) Certbot-Deploy-Hook: ruft den Wrapper bei jeder erfolgreichen Ausstellung/Renew auf
|
||||||
|
#install -d -m 0755 /etc/letsencrypt/renewal-hooks/deploy
|
||||||
|
#cat >/etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh <<'HOOK'
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#exec /usr/local/sbin/mw-deploy.sh
|
||||||
|
#HOOK
|
||||||
|
#chmod +x /etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh
|
||||||
|
#
|
||||||
|
#log "[✓] MailWolt Deploy-Hook eingerichtet"
|
||||||
|
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#set -euo pipefail
|
||||||
|
#source ./lib.sh
|
||||||
|
#
|
||||||
|
#install -d -m 0755 /etc/mailwolt
|
||||||
|
#cat >/etc/mailwolt/installer.env <<EOF
|
||||||
|
#UI_HOST=${UI_HOST}
|
||||||
|
#WEBMAIL_HOST=${WEBMAIL_HOST}
|
||||||
|
#MAIL_HOSTNAME=${MAIL_HOSTNAME}
|
||||||
|
#BASE_DOMAIN=${BASE_DOMAIN}
|
||||||
|
#LE_EMAIL=${LE_EMAIL:-admin@${BASE_DOMAIN}}
|
||||||
|
#APP_ENV=${APP_ENV:-production}
|
||||||
|
#EOF
|
||||||
|
#
|
||||||
|
#log "Let's Encrypt Deploy-Hooks und Wrapper anlegen …"
|
||||||
|
#
|
||||||
|
## 1) Wrapper, den Certbot bei Issue/Renew aufruft
|
||||||
|
#cat >/usr/local/sbin/mw-deploy.sh <<'WRAP'
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#set -euo pipefail
|
||||||
|
#
|
||||||
|
## Installer-Variablen laden (UI_HOST, WEBMAIL_HOST, MAIL_HOSTNAME, optional LE_EMAIL etc.)
|
||||||
|
#set +u
|
||||||
|
#[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
|
||||||
|
#set -u
|
||||||
|
#
|
||||||
|
#UI_HOST="${UI_HOST:-}"
|
||||||
|
#WEBMAIL_HOST="${WEBMAIL_HOST:-}"
|
||||||
|
#MAIL_HOSTNAME="${MAIL_HOSTNAME:-}"
|
||||||
|
#
|
||||||
|
#link_if() {
|
||||||
|
# local le_base="$1" target_dir="$2"
|
||||||
|
# local cert="${le_base}/fullchain.pem"
|
||||||
|
# local key="${le_base}/privkey.pem"
|
||||||
|
# [[ -s "$cert" && -s "$key" ]] || return 0
|
||||||
|
# install -d -m 0755 "$target_dir"
|
||||||
|
# ln -sf "$cert" "${target_dir}/fullchain.pem"
|
||||||
|
# ln -sf "$key" "${target_dir}/privkey.pem"
|
||||||
|
# chmod 644 "${target_dir}/fullchain.pem" 2>/dev/null || true
|
||||||
|
# chmod 600 "${target_dir}/privkey.pem" 2>/dev/null || true
|
||||||
|
# echo "[+] Linked ${target_dir} -> ${le_base}"
|
||||||
|
#}
|
||||||
|
#
|
||||||
|
## Nur für Domains arbeiten, die im aktuellen Lauf erneuert/ausgestellt wurden
|
||||||
|
#RDOMS=" ${RENEWED_DOMAINS:-} "
|
||||||
|
#
|
||||||
|
## UI
|
||||||
|
#if [[ -n "$UI_HOST" && "$RDOMS" == *" ${UI_HOST} "* ]]; then
|
||||||
|
# link_if "/etc/letsencrypt/live/${UI_HOST}" "/etc/ssl/ui"
|
||||||
|
#fi
|
||||||
|
## Webmail
|
||||||
|
#if [[ -n "$WEBMAIL_HOST" && "$RDOMS" == *" ${WEBMAIL_HOST} "* ]]; then
|
||||||
|
# link_if "/etc/letsencrypt/live/${WEBMAIL_HOST}" "/etc/ssl/webmail"
|
||||||
|
#fi
|
||||||
|
## MX
|
||||||
|
#if [[ -n "$MAIL_HOSTNAME" && "$RDOMS" == *" ${MAIL_HOSTNAME} "* ]]; then
|
||||||
|
# link_if "/etc/letsencrypt/live/${MAIL_HOSTNAME}" "/etc/ssl/mail"
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
## Optional: TLSA via Laravel, falls App schon vorhanden (sonst still überspringen)
|
||||||
|
#if command -v php >/dev/null 2>&1 && [ -d /var/www/mailwolt ]; then
|
||||||
|
# (cd /var/www/mailwolt && php artisan dns:tlsa:refresh) || true
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
## Nginx nur neu laden, wenn aktiv
|
||||||
|
#if systemctl is-active --quiet nginx; then
|
||||||
|
# systemctl reload nginx || true
|
||||||
|
#fi
|
||||||
|
#WRAP
|
||||||
|
#chmod +x /usr/local/sbin/mw-deploy.sh
|
||||||
|
#
|
||||||
|
## 2) Certbot-Deploy-Hooks einrichten (ruft nur den Wrapper auf)
|
||||||
|
#install -d -m 0755 /etc/letsencrypt/renewal-hooks/deploy
|
||||||
|
#cat >/etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh <<'HOOK'
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#exec /usr/local/sbin/mw-deploy.sh
|
||||||
|
#HOOK
|
||||||
|
#chmod +x /etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh
|
||||||
|
#
|
||||||
|
#log "[✓] MailWolt Deploy-Hook eingerichtet"
|
||||||
|
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#set -euo pipefail
|
||||||
|
#source ./lib.sh
|
||||||
|
#
|
||||||
|
#log "Let's Encrypt Deploy-Hooks und Wrapper anlegen …"
|
||||||
|
#
|
||||||
|
## 1) Wrapper-Skript, das Symlinks setzt und Nginx reloaded
|
||||||
|
#cat >/usr/local/sbin/mw-deploy.sh <<'WRAP'
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#set -euo pipefail
|
||||||
|
#
|
||||||
|
#link_if() {
|
||||||
|
# local le_base="$1" target_dir="$2"
|
||||||
|
# local cert="${le_base}/fullchain.pem"
|
||||||
|
# local key="${le_base}/privkey.pem"
|
||||||
|
# [[ -s "$cert" && -s "$key" ]] || return 0
|
||||||
|
# install -d -m 0755 "$target_dir"
|
||||||
|
# ln -sf "$cert" "${target_dir}/fullchain.pem"
|
||||||
|
# ln -sf "$key" "${target_dir}/privkey.pem"
|
||||||
|
# chmod 644 "${target_dir}/fullchain.pem" 2>/dev/null || true
|
||||||
|
# chmod 600 "${target_dir}/privkey.pem" 2>/dev/null || true
|
||||||
|
# echo "[+] Linked ${target_dir} -> ${le_base}"
|
||||||
|
#}
|
||||||
|
#
|
||||||
|
#UI_HOST="${UI_HOST:-}"
|
||||||
|
#WEBMAIL_HOST="${WEBMAIL_HOST:-}"
|
||||||
|
#MAIL_HOSTNAME="${MAIL_HOSTNAME:-}"
|
||||||
|
#
|
||||||
|
#[[ -n "$UI_HOST" ]] && link_if "/etc/letsencrypt/live/${UI_HOST}" "/etc/ssl/ui"
|
||||||
|
#[[ -n "$WEBMAIL_HOST" ]] && link_if "/etc/letsencrypt/live/${WEBMAIL_HOST}" "/etc/ssl/webmail"
|
||||||
|
#[[ -n "$MAIL_HOSTNAME" ]] && link_if "/etc/letsencrypt/live/${MAIL_HOSTNAME}" "/etc/ssl/mail"
|
||||||
|
#
|
||||||
|
#if systemctl is-active --quiet nginx; then
|
||||||
|
# systemctl reload nginx || true
|
||||||
|
#fi
|
||||||
|
#WRAP
|
||||||
|
#
|
||||||
|
#chmod +x /usr/local/sbin/mw-deploy.sh
|
||||||
|
#
|
||||||
|
## 2) Certbot Deploy-Hook-Verzeichnis + Symlink für Renewals
|
||||||
|
#install -d -m 0755 /etc/letsencrypt/renewal-hooks/deploy
|
||||||
|
#cat >/etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh <<'HOOK'
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#exec /usr/local/sbin/mw-deploy.sh
|
||||||
|
#HOOK
|
||||||
|
#chmod +x /etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh
|
||||||
|
#
|
||||||
|
#log "[✓] MailWolt Deploy-Hook eingerichtet"
|
||||||
|
#
|
||||||
|
###!/usr/bin/env bash
|
||||||
|
##set -euo pipefail
|
||||||
|
##source ./lib.sh
|
||||||
|
##
|
||||||
|
### ────────────────────────────────────────────────────────────────────────────
|
||||||
|
### 21-le-deploy-hook.sh
|
||||||
|
### • legt /etc/mailwolt/installer.env an (falls fehlt)
|
||||||
|
### • erzeugt Deploy-Hooks:
|
||||||
|
### - 50-mailwolt-symlinks.sh → verlinkt LE-Zerts nach /etc/ssl/{ui,webmail,mail}
|
||||||
|
### - 60-mailwolt-tlsa.sh → aktualisiert TLSA (3 1 1) für MX bei jedem Renew
|
||||||
|
### • KEIN Reload von Postfix/Dovecot (kommt später im Installer)
|
||||||
|
### ────────────────────────────────────────────────────────────────────────────
|
||||||
|
##
|
||||||
|
### 0) Hostnamen persistent speichern (für spätere Deploys)
|
||||||
|
##install -d -m 0755 /etc/mailwolt
|
||||||
|
##if [[ ! -f /etc/mailwolt/installer.env ]]; then
|
||||||
|
## cat >/etc/mailwolt/installer.env <<EOF
|
||||||
|
##UI_HOST=${UI_HOST}
|
||||||
|
##WEBMAIL_HOST=${WEBMAIL_HOST}
|
||||||
|
##MAIL_HOSTNAME=${MAIL_HOSTNAME}
|
||||||
|
##EOF
|
||||||
|
## echo "[+] /etc/mailwolt/installer.env erstellt."
|
||||||
|
##fi
|
||||||
|
##
|
||||||
|
### 1) Deploy-Hooks-Verzeichnis anlegen
|
||||||
|
##install -d -m 0755 /etc/letsencrypt/renewal-hooks/deploy
|
||||||
|
##
|
||||||
|
### ────────────────────────────────────────────────────────────────────────────
|
||||||
|
### 2) 50-mailwolt-symlinks.sh
|
||||||
|
### ────────────────────────────────────────────────────────────────────────────
|
||||||
|
##cat >/etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh <<HOOK
|
||||||
|
###!/usr/bin/env bash
|
||||||
|
##set -euo pipefail
|
||||||
|
##
|
||||||
|
##UI_LE="/etc/letsencrypt/live/${UI_HOST}"
|
||||||
|
##WEBMAIL_LE="/etc/letsencrypt/live/${WEBMAIL_HOST}"
|
||||||
|
##MX_LE="/etc/letsencrypt/live/${MAIL_HOSTNAME}"
|
||||||
|
##
|
||||||
|
##UI_SSL_DIR="/etc/ssl/ui"
|
||||||
|
##WEBMAIL_SSL_DIR="/etc/ssl/webmail"
|
||||||
|
##MAIL_SSL_DIR="/etc/ssl/mail"
|
||||||
|
##
|
||||||
|
### Zielverzeichnisse anlegen (einmalig)
|
||||||
|
##install -d -m 0755 "\$UI_SSL_DIR" "\$WEBMAIL_SSL_DIR" "\$MAIL_SSL_DIR"
|
||||||
|
##
|
||||||
|
##link_if() {
|
||||||
|
## local le_base="\$1" target_dir="\$2"
|
||||||
|
## local cert="\${le_base}/fullchain.pem"
|
||||||
|
## local key="\${le_base}/privkey.pem"
|
||||||
|
## [[ -s "\$cert" && -s "\$key" ]] || return 0
|
||||||
|
## ln -sf "\$cert" "\${target_dir}/fullchain.pem"
|
||||||
|
## ln -sf "\$key" "\${target_dir}/privkey.pem"
|
||||||
|
## chmod 644 "\${target_dir}/fullchain.pem" 2>/dev/null || true
|
||||||
|
## chmod 600 "\${target_dir}/privkey.pem" 2>/dev/null || true
|
||||||
|
## echo "[+] Linked \${target_dir} -> \${le_base}"
|
||||||
|
##}
|
||||||
|
##
|
||||||
|
### Verlinken (nur wenn Host konfiguriert)
|
||||||
|
##[[ -n "${UI_HOST}" ]] && link_if "\$UI_LE" "\$UI_SSL_DIR"
|
||||||
|
##[[ -n "${WEBMAIL_HOST}" ]] && link_if "\$WEBMAIL_LE" "\$WEBMAIL_SSL_DIR"
|
||||||
|
##[[ -n "${MAIL_HOSTNAME}" ]] && link_if "\$MX_LE" "\$MAIL_SSL_DIR"
|
||||||
|
##
|
||||||
|
### Nur reloaden, wenn Nginx aktiv ist (Installer startet ihn später erst)
|
||||||
|
##if systemctl is-active --quiet nginx; then
|
||||||
|
## systemctl reload nginx || true
|
||||||
|
##fi
|
||||||
|
##HOOK
|
||||||
|
##chmod +x /etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh
|
||||||
|
##
|
||||||
|
### ────────────────────────────────────────────────────────────────────────────
|
||||||
|
### 3) 60-mailwolt-tlsa.sh
|
||||||
|
### → nutzt Laravel, falls vorhanden; sonst Fallback mit OpenSSL.
|
||||||
|
### → schreibt nur, wenn sich der Hash geändert hat (idempotent)
|
||||||
|
### ────────────────────────────────────────────────────────────────────────────
|
||||||
|
##cat >/etc/letsencrypt/renewal-hooks/deploy/60-mailwolt-tlsa.sh <<'HOOK'
|
||||||
|
###!/usr/bin/env bash
|
||||||
|
##set -euo pipefail
|
||||||
|
##
|
||||||
|
### installer.env lesen
|
||||||
|
##set +u
|
||||||
|
##[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
|
||||||
|
##set -u
|
||||||
|
##
|
||||||
|
##APP_ENV_VAL="${APP_ENV:-production}"
|
||||||
|
##BASE_DOMAIN_VAL="${BASE_DOMAIN:-example.com}"
|
||||||
|
##
|
||||||
|
##case "$APP_ENV_VAL" in
|
||||||
|
## local|dev|development) exit 0 ;;
|
||||||
|
##esac
|
||||||
|
##[ "$BASE_DOMAIN_VAL" = "example.com" ] && exit 0
|
||||||
|
##
|
||||||
|
##MX_HOST="${MAIL_HOSTNAME:-}"
|
||||||
|
##SERVICE="_25._tcp"
|
||||||
|
##DNS_DIR="/etc/mailwolt/dns"
|
||||||
|
##OUT_FILE="${DNS_DIR}/${MX_HOST}.tlsa.txt"
|
||||||
|
##
|
||||||
|
### Nur reagieren, wenn MX-Zertifikat betroffen war
|
||||||
|
##case " ${RENEWED_DOMAINS:-} " in
|
||||||
|
## *" ${MX_HOST} "*) ;;
|
||||||
|
## *) exit 0 ;;
|
||||||
|
##esac
|
||||||
|
##
|
||||||
|
##CERT="${RENEWED_LINEAGE}/fullchain.pem"
|
||||||
|
##[ -s "$CERT" ] || exit 0
|
||||||
|
##
|
||||||
|
### Wenn Laravel vorhanden ist → interner Command (DB + Datei idempotent)
|
||||||
|
##if command -v php >/dev/null 2>&1 && [ -d /var/www/mailwolt ]; then
|
||||||
|
## cd /var/www/mailwolt || exit 0
|
||||||
|
## php artisan dns:tlsa:refresh || true
|
||||||
|
## exit 0
|
||||||
|
##fi
|
||||||
|
##
|
||||||
|
### Fallback: nur Datei aktualisieren, wenn Hash sich ändert
|
||||||
|
##HASH="$(openssl x509 -in "$CERT" -noout -pubkey \
|
||||||
|
## | openssl pkey -pubin -outform DER \
|
||||||
|
## | openssl dgst -sha256 | sed 's/^.*= //')"
|
||||||
|
##NEW_LINE="${SERVICE}.${MX_HOST}. IN TLSA 3 1 1 ${HASH}"
|
||||||
|
##
|
||||||
|
##mkdir -p "$DNS_DIR"
|
||||||
|
##
|
||||||
|
##if [ -r "$OUT_FILE" ] && grep -q "IN TLSA" "$OUT_FILE"; then
|
||||||
|
## if grep -q "$HASH" "$OUT_FILE"; then
|
||||||
|
## echo "[TLSA] Unverändert – kein Update nötig."
|
||||||
|
## exit 0
|
||||||
|
## fi
|
||||||
|
##fi
|
||||||
|
##
|
||||||
|
##echo "$NEW_LINE" > "$OUT_FILE"
|
||||||
|
##echo "[TLSA] Aktualisiert: $NEW_LINE"
|
||||||
|
##HOOK
|
||||||
|
##chmod +x /etc/letsencrypt/renewal-hooks/deploy/60-mailwolt-tlsa.sh
|
||||||
|
##
|
||||||
|
### ────────────────────────────────────────────────────────────────────────────
|
||||||
|
##echo "[✓] Deploy-Hooks installiert."
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
log "Installiere DKIM-Helper …"
|
||||||
|
|
||||||
|
install -d -m 0755 /usr/local/sbin
|
||||||
|
|
||||||
|
cat >/usr/local/sbin/mailwolt-install-dkim <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DOMAIN="$1" # z.B. sysmail.toastra.com
|
||||||
|
SELECTOR="${2:-mwl1}"
|
||||||
|
|
||||||
|
[[ -n "$DOMAIN" ]] || { echo "Usage: $0 <domain> [selector]"; exit 2; }
|
||||||
|
|
||||||
|
KEYDIR="/etc/opendkim/keys/${DOMAIN}"
|
||||||
|
PRIV="${KEYDIR}/${SELECTOR}.private"
|
||||||
|
TXT="${KEYDIR}/${SELECTOR}.txt"
|
||||||
|
|
||||||
|
install -d -m 0750 -o opendkim -g opendkim "$KEYDIR"
|
||||||
|
|
||||||
|
if [[ ! -s "$PRIV" ]]; then
|
||||||
|
opendkim-genkey -b 2048 -s "$SELECTOR" -d "$DOMAIN" -D "$KEYDIR"
|
||||||
|
chown opendkim:opendkim "$PRIV"
|
||||||
|
chmod 600 "$PRIV"
|
||||||
|
fi
|
||||||
|
|
||||||
|
grep -q "^${SELECTOR}\._domainkey\.${DOMAIN} " /etc/opendkim/KeyTable 2>/dev/null \
|
||||||
|
|| echo "${SELECTOR}._domainkey.${DOMAIN} ${DOMAIN}:${SELECTOR}:${PRIV}" >> /etc/opendkim/KeyTable
|
||||||
|
|
||||||
|
grep -q "^\*@${DOMAIN} " /etc/opendkim/SigningTable 2>/dev/null \
|
||||||
|
|| echo "*@${DOMAIN} ${SELECTOR}._domainkey.${DOMAIN}" >> /etc/opendkim/SigningTable
|
||||||
|
|
||||||
|
install -d -m 0755 /etc/mailwolt/dns
|
||||||
|
[[ -s "$TXT" ]] && cp -f "$TXT" "/etc/mailwolt/dns/dkim-${DOMAIN}.txt" || true
|
||||||
|
|
||||||
|
systemctl restart opendkim
|
||||||
|
EOF
|
||||||
|
|
||||||
|
log "[✓] DKIM-Helper installiert: /usr/local/sbin/mailwolt-install-dkim"
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
log "MariaDB vorbereiten …"
|
||||||
|
systemctl enable --now mariadb
|
||||||
|
mysql -uroot <<SQL
|
||||||
|
CREATE DATABASE IF NOT EXISTS ${DB_NAME} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
CREATE USER IF NOT EXISTS '${DB_USER}'@'localhost' IDENTIFIED BY '${DB_PASS}';
|
||||||
|
CREATE USER IF NOT EXISTS '${DB_USER}'@'127.0.0.1' IDENTIFIED BY '${DB_PASS}';
|
||||||
|
GRANT ALL PRIVILEGES ON ${DB_NAME}.* TO '${DB_USER}'@'localhost';
|
||||||
|
GRANT ALL PRIVILEGES ON ${DB_NAME}.* TO '${DB_USER}'@'127.0.0.1';
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
|
SQL
|
||||||
|
|
@ -0,0 +1,133 @@
|
||||||
|
#!/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 "Postfix konfigurieren …"
|
||||||
|
|
||||||
|
# --- TLS-Dateirechte (falls du sie in /etc/mailwolt/ssl spiegelst) -----------
|
||||||
|
if [[ -e "${MAIL_KEY}" ]]; then
|
||||||
|
chgrp -R postfix /etc/mailwolt/ssl || true
|
||||||
|
chmod 750 /etc/mailwolt/ssl || true
|
||||||
|
chmod 640 /etc/mailwolt/ssl/key.pem /etc/mailwolt/ssl/cert.pem || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Basiskonfiguration -------------------------------------------------------
|
||||||
|
/usr/sbin/postconf -e "myhostname = ${MAIL_HOSTNAME}"
|
||||||
|
/usr/sbin/postconf -e "myorigin = \$myhostname"
|
||||||
|
/usr/sbin/postconf -e "mydestination = "
|
||||||
|
/usr/sbin/postconf -e "inet_interfaces = all"
|
||||||
|
/usr/sbin/postconf -e "inet_protocols = all"
|
||||||
|
/usr/sbin/postconf -e "smtpd_banner = \$myhostname ESMTP"
|
||||||
|
|
||||||
|
# --- TLS ----------------------------------------------------------------------
|
||||||
|
/usr/sbin/postconf -e "smtpd_tls_cert_file = ${MAIL_CERT}"
|
||||||
|
/usr/sbin/postconf -e "smtpd_tls_key_file = ${MAIL_KEY}"
|
||||||
|
/usr/sbin/postconf -e "smtpd_tls_security_level = may"
|
||||||
|
/usr/sbin/postconf -e "smtpd_use_tls = yes"
|
||||||
|
/usr/sbin/postconf -e "smtpd_tls_received_header = yes"
|
||||||
|
/usr/sbin/postconf -e "smtpd_tls_protocols = !SSLv2,!SSLv3"
|
||||||
|
/usr/sbin/postconf -e "smtpd_tls_mandatory_protocols = !SSLv2,!SSLv3"
|
||||||
|
/usr/sbin/postconf -e "smtpd_tls_loglevel = 1"
|
||||||
|
/usr/sbin/postconf -e "smtp_tls_security_level = may"
|
||||||
|
/usr/sbin/postconf -e "smtp_tls_loglevel = 1"
|
||||||
|
|
||||||
|
DH_FILE="/etc/ssl/private/dhparam.pem"
|
||||||
|
if [[ ! -s "$DH_FILE" ]]; then
|
||||||
|
log "Generiere 2048-Bit DH-Parameter …"
|
||||||
|
openssl dhparam -out "$DH_FILE" 2048
|
||||||
|
chmod 600 "$DH_FILE"
|
||||||
|
chown root:root "$DH_FILE"
|
||||||
|
fi
|
||||||
|
/usr/sbin/postconf -e "smtpd_tls_dh1024_param_file = ${DH_FILE}"
|
||||||
|
/usr/sbin/postconf -e "smtpd_tls_dh1024_param_file = ${DH_FILE}"
|
||||||
|
/usr/sbin/postconf -e "smtpd_tls_eecdh_grade = strong"
|
||||||
|
/usr/sbin/postconf -e "tls_preempt_cipherlist = yes"
|
||||||
|
|
||||||
|
# Nur moderne TLS-Versionen (auch für ausgehendes SMTP)
|
||||||
|
# (überschreibt die älteren Zeilen oben)
|
||||||
|
/usr/sbin/postconf -e "smtpd_tls_protocols = !SSLv2,!SSLv3,!TLSv1,!TLSv1.1"
|
||||||
|
/usr/sbin/postconf -e "smtpd_tls_mandatory_protocols = !SSLv2,!SSLv3,!TLSv1,!TLSv1.1"
|
||||||
|
/usr/sbin/postconf -e "smtp_tls_protocols = !SSLv2,!SSLv3,!TLSv1,!TLSv1.1"
|
||||||
|
|
||||||
|
# Hohe Cipher, alte raus
|
||||||
|
/usr/sbin/postconf -e "smtpd_tls_ciphers = high"
|
||||||
|
/usr/sbin/postconf -e "smtp_tls_ciphers = high"
|
||||||
|
/usr/sbin/postconf -e "smtpd_tls_exclude_ciphers = aNULL,eNULL,MD5,RC4,DES,3DES"
|
||||||
|
/usr/sbin/postconf -e "smtp_tls_exclude_ciphers = aNULL,eNULL,MD5,RC4,DES,3DES"
|
||||||
|
|
||||||
|
# --- SMTP Sicherheit ----------------------------------------------------------
|
||||||
|
/usr/sbin/postconf -e "disable_vrfy_command = yes"
|
||||||
|
/usr/sbin/postconf -e "smtpd_helo_required = yes"
|
||||||
|
|
||||||
|
# --- Milter -------------------------------------------------------------------
|
||||||
|
/usr/sbin/postconf -e "milter_default_action = accept"
|
||||||
|
/usr/sbin/postconf -e "milter_protocol = 6"
|
||||||
|
/usr/sbin/postconf -e "smtpd_milters = inet:127.0.0.1:11332, inet:127.0.0.1:8891"
|
||||||
|
/usr/sbin/postconf -e "non_smtpd_milters = inet:127.0.0.1:11332, inet:127.0.0.1:8891"
|
||||||
|
|
||||||
|
# --- SASL Auth via Dovecot ----------------------------------------------------
|
||||||
|
/usr/sbin/postconf -e "smtpd_sasl_type = dovecot"
|
||||||
|
/usr/sbin/postconf -e "smtpd_sasl_path = private/auth"
|
||||||
|
/usr/sbin/postconf -e "smtpd_sasl_auth_enable = yes"
|
||||||
|
/usr/sbin/postconf -e "smtpd_sasl_security_options = noanonymous"
|
||||||
|
|
||||||
|
# --- Recipient & Relay Restriction --------------------------------------------
|
||||||
|
/usr/sbin/postconf -e "smtpd_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination"
|
||||||
|
/usr/sbin/postconf -e "smtpd_relay_restrictions = permit_mynetworks, reject_unauth_destination"
|
||||||
|
|
||||||
|
# --- Listener / Master.cf Definition ------------------------------------------
|
||||||
|
/usr/sbin/postconf -M "smtp/inet=smtp inet n - n - - smtpd -o smtpd_peername_lookup=no -o smtpd_timeout=30s"
|
||||||
|
/usr/sbin/postconf -M "submission/inet=submission inet n - n - - smtpd -o syslog_name=postfix/submission -o smtpd_peername_lookup=no -o smtpd_tls_security_level=encrypt -o smtpd_tls_auth_only=yes -o smtpd_sasl_auth_enable=yes -o smtpd_relay_restrictions=permit_sasl_authenticated,reject -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject"
|
||||||
|
/usr/sbin/postconf -M "smtps/inet=smtps inet n - n - - smtpd -o syslog_name=postfix/smtps -o smtpd_peername_lookup=no -o smtpd_tls_wrappermode=yes -o smtpd_tls_auth_only=yes -o smtpd_sasl_auth_enable=yes -o smtpd_relay_restrictions=permit_sasl_authenticated,reject -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject"
|
||||||
|
|
||||||
|
# postscreen ggf. deaktivieren
|
||||||
|
sed -i 's/^[[:space:]]*smtp[[:space:]]\+inet[[:space:]]\+.*postscreen/# &/' /etc/postfix/master.cf || true
|
||||||
|
|
||||||
|
# --- SQL Maps (Verzeichnis zuerst!) -------------------------------------------
|
||||||
|
install -d -o root -g postfix -m 750 /etc/postfix/sql
|
||||||
|
|
||||||
|
# Domains
|
||||||
|
cat > /etc/postfix/sql/mysql-virtual-domains.cf <<CONF
|
||||||
|
hosts = 127.0.0.1
|
||||||
|
user = ${DB_USER}
|
||||||
|
password = ${DB_PASS}
|
||||||
|
dbname = ${DB_NAME}
|
||||||
|
query = SELECT 1 FROM domains WHERE domain = '%s' AND is_active = 1 LIMIT 1;
|
||||||
|
CONF
|
||||||
|
chown root:postfix /etc/postfix/sql/mysql-virtual-domains.cf
|
||||||
|
chmod 640 /etc/postfix/sql/mysql-virtual-domains.cf
|
||||||
|
|
||||||
|
# Mailboxen
|
||||||
|
cat > /etc/postfix/sql/mysql-virtual-mailbox-maps.cf <<CONF
|
||||||
|
hosts = 127.0.0.1
|
||||||
|
user = ${DB_USER}
|
||||||
|
password = ${DB_PASS}
|
||||||
|
dbname = ${DB_NAME}
|
||||||
|
query = SELECT 1 FROM mail_users u JOIN domains d ON d.id = u.domain_id WHERE u.email = '%s' AND u.is_active = 1 AND u.can_login = 1 AND u.password_hash IS NOT NULL AND d.is_active = 1 LIMIT 1;
|
||||||
|
CONF
|
||||||
|
chown root:postfix /etc/postfix/sql/mysql-virtual-mailbox-maps.cf
|
||||||
|
chmod 640 /etc/postfix/sql/mysql-virtual-mailbox-maps.cf
|
||||||
|
|
||||||
|
# Aliase
|
||||||
|
cat > /etc/postfix/sql/mysql-virtual-alias-maps.cf <<CONF
|
||||||
|
hosts = 127.0.0.1
|
||||||
|
user = ${DB_USER}
|
||||||
|
password = ${DB_PASS}
|
||||||
|
dbname = ${DB_NAME}
|
||||||
|
query = SELECT COALESCE(mu.email, r.email) AS destination FROM mail_aliases a JOIN domains d ON d.id = a.domain_id JOIN mail_alias_recipients r ON r.alias_id = a.id LEFT JOIN mail_users mu ON mu.id = r.mail_user_id WHERE d.domain = SUBSTRING_INDEX('%s','@',-1) AND a.local = SUBSTRING_INDEX('%s','@', 1) AND a.is_active = 1 AND d.is_active = 1 AND (mu.email IS NOT NULL OR r.email IS NOT NULL) ORDER BY r.position ASC;
|
||||||
|
CONF
|
||||||
|
chown root:postfix /etc/postfix/sql/mysql-virtual-alias-maps.cf
|
||||||
|
chmod 640 /etc/postfix/sql/mysql-virtual-alias-maps.cf
|
||||||
|
|
||||||
|
# Aktivieren
|
||||||
|
/usr/sbin/postconf -e "virtual_mailbox_domains = proxy:mysql:/etc/postfix/sql/mysql-virtual-domains.cf"
|
||||||
|
/usr/sbin/postconf -e "virtual_mailbox_maps = proxy:mysql:/etc/postfix/sql/mysql-virtual-mailbox-maps.cf"
|
||||||
|
/usr/sbin/postconf -e "virtual_alias_maps = proxy:mysql:/etc/postfix/sql/mysql-virtual-alias-maps.cf"
|
||||||
|
/usr/sbin/postconf -e "virtual_transport = lmtp:unix:private/dovecot-lmtp"
|
||||||
|
|
||||||
|
# --- Dienst aktivieren & neu laden --------------------------------------------
|
||||||
|
#systemctl enable postfix >/dev/null 2>&1 || true
|
||||||
|
|
@ -0,0 +1,247 @@
|
||||||
|
#!/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 …"
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# 1) vmail-Benutzer/Gruppe & Mailspool vorbereiten (DYNAMIC UID!)
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Sicherstellen, dass die Gruppe 'mail' existiert (auf Debian/Ubuntu idR vorhanden)
|
||||||
|
getent group mail >/dev/null || groupadd -g 8 mail || true
|
||||||
|
|
||||||
|
# vmail anlegen, wenn er fehlt. Bevorzugt UID 109, falls frei – sonst automatisch.
|
||||||
|
if ! getent passwd vmail >/dev/null; then
|
||||||
|
if ! getent passwd 109 >/dev/null; then
|
||||||
|
useradd -u 109 -g mail -d /var/mail -M -s /usr/sbin/nologin vmail
|
||||||
|
else
|
||||||
|
useradd -g mail -d /var/mail -M -s /usr/sbin/nologin vmail
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Tatsächliche vmail-UID ermitteln (wird unten in die Dovecot-Config geschrieben)
|
||||||
|
VMAIL_UID="$(id -u vmail)"
|
||||||
|
|
||||||
|
# Mailspool-Basis
|
||||||
|
install -d -m 0770 -o vmail -g mail /var/mail/vhosts
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# 2) Dovecot Grundgerüst
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Hauptdatei
|
||||||
|
install -d -m 0755 /etc/dovecot/conf.d
|
||||||
|
cat > /etc/dovecot/dovecot.conf <<'CONF'
|
||||||
|
!include_try /etc/dovecot/conf.d/*.conf
|
||||||
|
CONF
|
||||||
|
|
||||||
|
# Mail-Location & Namespace + UID-Grenzen
|
||||||
|
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
|
||||||
|
mail_access_groups = mail
|
||||||
|
first_valid_uid = ${VMAIL_UID}
|
||||||
|
last_valid_uid = ${VMAIL_UID}
|
||||||
|
CONF
|
||||||
|
|
||||||
|
cat > /etc/dovecot/conf.d/15-mailboxes.conf <<'CONF'
|
||||||
|
namespace inbox {
|
||||||
|
inbox = yes
|
||||||
|
|
||||||
|
mailbox Drafts {
|
||||||
|
special_use = \Drafts
|
||||||
|
auto = subscribe
|
||||||
|
}
|
||||||
|
mailbox Junk {
|
||||||
|
special_use = \Junk
|
||||||
|
auto = subscribe
|
||||||
|
}
|
||||||
|
mailbox Trash {
|
||||||
|
special_use = \Trash
|
||||||
|
auto = subscribe
|
||||||
|
}
|
||||||
|
mailbox Sent {
|
||||||
|
special_use = \Sent
|
||||||
|
auto = subscribe
|
||||||
|
}
|
||||||
|
mailbox Archive {
|
||||||
|
special_use = \Archive
|
||||||
|
auto = create
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
|
||||||
|
auth_cache_size = 10M
|
||||||
|
auth_cache_ttl = 1 hour
|
||||||
|
CONF
|
||||||
|
|
||||||
|
# SQL-Anbindung (Passwörter aus App-DB)
|
||||||
|
cat > /etc/dovecot/dovecot-sql.conf.ext <<CONF
|
||||||
|
driver = mysql
|
||||||
|
connect = host=127.0.0.1 dbname=${DB_NAME} user=${DB_USER} password=${DB_PASS}
|
||||||
|
default_pass_scheme = BLF-CRYPT
|
||||||
|
password_query = SELECT u.email AS user, u.password_hash AS password FROM mail_users u JOIN domains d ON d.id = u.domain_id WHERE u.email = '%u' AND u.is_active = 1 AND u.can_login = 1 AND u.password_hash IS NOT NULL AND d.is_active = 1 LIMIT 1;
|
||||||
|
CONF
|
||||||
|
chown root:dovecot /etc/dovecot/dovecot-sql.conf.ext
|
||||||
|
chmod 640 /etc/dovecot/dovecot-sql.conf.ext
|
||||||
|
|
||||||
|
# Auth-SQL → userdb static auf vmail:mail (Home unter /var/mail/vhosts/%d/%n)
|
||||||
|
cat > /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=mail 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
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# 3) IMAP Optimierung (iOS/IDLE-freundlich)
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
cat > /etc/dovecot/conf.d/20-imap.conf <<'CONF'
|
||||||
|
# IMAP-spezifische Einstellungen
|
||||||
|
|
||||||
|
imap_idle_notify_interval = 2 mins
|
||||||
|
imap_hibernate_timeout = 0
|
||||||
|
|
||||||
|
protocol imap {
|
||||||
|
mail_max_userip_connections = 20
|
||||||
|
imap_logout_format = in=%i out=%o deleted=%{deleted} expunged=%{expunged}
|
||||||
|
}
|
||||||
|
CONF
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# 4) Master Services (LMTP, AUTH, IMAP, POP3, STATS)
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
unix_listener auth-userdb {
|
||||||
|
mode = 0660
|
||||||
|
user = vmail
|
||||||
|
group = mail
|
||||||
|
}
|
||||||
|
process_limit = 1
|
||||||
|
}
|
||||||
|
service imap-login {
|
||||||
|
inet_listener imap {
|
||||||
|
port = 143
|
||||||
|
}
|
||||||
|
inet_listener imaps {
|
||||||
|
port = 993
|
||||||
|
ssl = yes
|
||||||
|
}
|
||||||
|
process_limit = 128
|
||||||
|
process_min_avail = 10
|
||||||
|
service_count = 0
|
||||||
|
vsz_limit = 512M
|
||||||
|
}
|
||||||
|
service pop3-login {
|
||||||
|
inet_listener pop3 {
|
||||||
|
port = 110
|
||||||
|
}
|
||||||
|
inet_listener pop3s {
|
||||||
|
port = 995
|
||||||
|
ssl = yes
|
||||||
|
}
|
||||||
|
process_limit = 50
|
||||||
|
service_count = 0
|
||||||
|
}
|
||||||
|
CONF
|
||||||
|
|
||||||
|
# --- Dovecot: doveadm-server für App-Zugriff ---
|
||||||
|
cat >/etc/dovecot/conf.d/99-mailwolt-perms.conf <<'CONF'
|
||||||
|
service auth {
|
||||||
|
unix_listener auth-userdb {
|
||||||
|
mode = 0660
|
||||||
|
user = vmail
|
||||||
|
group = mail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
service stats {
|
||||||
|
unix_listener stats-reader {
|
||||||
|
mode = 0660
|
||||||
|
user = vmail
|
||||||
|
group = mail
|
||||||
|
}
|
||||||
|
unix_listener stats-writer {
|
||||||
|
mode = 0660
|
||||||
|
user = vmail
|
||||||
|
group = mail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CONF
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# 5) SSL-Konfiguration (ohne DH-Param-Erzeugung)
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
DOVECOT_SSL_CONF="/etc/dovecot/conf.d/10-ssl.conf"
|
||||||
|
touch "$DOVECOT_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
|
||||||
|
grep -q '^ssl_min_protocol' "$DOVECOT_SSL_CONF" || echo "ssl_min_protocol = TLSv1.2" >> "$DOVECOT_SSL_CONF"
|
||||||
|
grep -q '^ssl_prefer_server_ciphers' "$DOVECOT_SSL_CONF" || echo "ssl_prefer_server_ciphers = yes" >> "$DOVECOT_SSL_CONF"
|
||||||
|
|
||||||
|
grep -q '^ssl_dh' "$DOVECOT_SSL_CONF" || echo "ssl_dh = </etc/ssl/private/dhparam.pem" >> "$DOVECOT_SSL_CONF"
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# 6) Verzeichnisse & Rechte prüfen
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
mkdir -p /var/spool/postfix/private
|
||||||
|
chown root:root /var/spool/postfix
|
||||||
|
chmod 0755 /var/spool/postfix
|
||||||
|
chown postfix:postfix /var/spool/postfix/private
|
||||||
|
chmod 0755 /var/spool/postfix/private
|
||||||
|
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# 7) Abschluss
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
log "Dovecot-Konfiguration abgeschlossen."
|
||||||
|
|
@ -0,0 +1,319 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
log "Rspamd + OpenDKIM einrichten …"
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────
|
||||||
|
# ENV laden
|
||||||
|
# ──────────────────────────────────────────────────────────────
|
||||||
|
set +u
|
||||||
|
[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
|
||||||
|
set -u
|
||||||
|
|
||||||
|
BASE_DOMAIN="${BASE_DOMAIN:-example.com}"
|
||||||
|
SYSMAIL_DOMAIN="${SYSMAIL_DOMAIN:-sysmail.${BASE_DOMAIN}}" # z.B. sysmail.example.com
|
||||||
|
DKIM_ENABLE="${DKIM_ENABLE:-1}" # 1=OpenDKIM aktiv
|
||||||
|
DKIM_SELECTOR="${DKIM_SELECTOR:-mwl1}" # z.B. mwl1
|
||||||
|
DKIM_GENERATE="${DKIM_GENERATE:-0}" # 1=Key generieren, falls fehlt
|
||||||
|
RSPAMD_CONTROLLER_PASSWORD="${RSPAMD_CONTROLLER_PASSWORD:-admin}"
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────
|
||||||
|
# Rspamd (Controller + Milter)
|
||||||
|
# ──────────────────────────────────────────────────────────────
|
||||||
|
install -d -m 0750 /etc/rspamd/local.d
|
||||||
|
|
||||||
|
if command -v rspamadm >/dev/null 2>&1; then
|
||||||
|
RSPAMD_HASH="$(rspamadm pw -p "${RSPAMD_CONTROLLER_PASSWORD}")"
|
||||||
|
else
|
||||||
|
RSPAMD_HASH="${RSPAMD_CONTROLLER_PASSWORD}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat >/etc/rspamd/local.d/worker-controller.inc <<CONF
|
||||||
|
worker "controller" {
|
||||||
|
bind_socket = "127.0.0.1:11334";
|
||||||
|
password = "${RSPAMD_HASH}";
|
||||||
|
}
|
||||||
|
CONF
|
||||||
|
|
||||||
|
cat >/etc/rspamd/local.d/statistic.conf <<CONF
|
||||||
|
classifier "bayes" {
|
||||||
|
backend = "redis";
|
||||||
|
autolearn = true;
|
||||||
|
autolearn_threshold = 6.0;
|
||||||
|
ham_symbols = ["BAYES_HAM"];
|
||||||
|
spam_symbols = ["BAYES_SPAM"];
|
||||||
|
min_learns = 10;
|
||||||
|
store_tokens = true;
|
||||||
|
per_user = false;
|
||||||
|
}
|
||||||
|
CONF
|
||||||
|
|
||||||
|
cat >/etc/rspamd/local.d/worker-proxy.inc <<'CONF'
|
||||||
|
worker "proxy" {
|
||||||
|
bind_socket = "127.0.0.1:11332";
|
||||||
|
milter = yes;
|
||||||
|
timeout = 120s;
|
||||||
|
|
||||||
|
upstream "scan" {
|
||||||
|
default = yes;
|
||||||
|
self_scan = yes;
|
||||||
|
servers = "127.0.0.1:11333";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CONF
|
||||||
|
|
||||||
|
cat >/etc/rspamd/local.d/worker-normal.inc <<'CONF'
|
||||||
|
worker "normal" {
|
||||||
|
bind_socket = "127.0.0.1:11333";
|
||||||
|
}
|
||||||
|
CONF
|
||||||
|
|
||||||
|
cat >/etc/rspamd/local.d/milter_headers.conf <<'CONF'
|
||||||
|
use = ["authentication-results"];
|
||||||
|
header = "Authentication-Results";
|
||||||
|
CONF
|
||||||
|
|
||||||
|
cat >/etc/rspamd/local.d/options.inc <<'CONF'
|
||||||
|
dns {
|
||||||
|
servers = ["9.9.9.9:53", "1.1.1.1:53"];
|
||||||
|
timeout = 5s;
|
||||||
|
retransmits = 2;
|
||||||
|
}
|
||||||
|
CONF
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────
|
||||||
|
# Rspamd Redis-Konfiguration
|
||||||
|
# ──────────────────────────────────────────────────────────────
|
||||||
|
log "Rspamd Redis konfigurieren …"
|
||||||
|
|
||||||
|
: "${REDIS_PASS:=}"
|
||||||
|
|
||||||
|
cat >/etc/rspamd/local.d/redis.conf <<CONF
|
||||||
|
servers = "127.0.0.1:6379";
|
||||||
|
${REDIS_PASS:+password = "${REDIS_PASS}";}
|
||||||
|
db = 0;
|
||||||
|
CONF
|
||||||
|
|
||||||
|
# Eigentümer und Rechte setzen
|
||||||
|
chown root:_rspamd /etc/rspamd/local.d /etc/rspamd/local.d/redis.conf
|
||||||
|
chmod 750 /etc/rspamd/local.d
|
||||||
|
chmod 640 /etc/rspamd/local.d/redis.conf
|
||||||
|
|
||||||
|
# Testweise prüfen, ob Redis erreichbar ist (nicht kritisch)
|
||||||
|
if command -v redis-cli >/dev/null 2>&1; then
|
||||||
|
if [[ -n "${REDIS_PASS}" ]]; then
|
||||||
|
if redis-cli -h 127.0.0.1 -p 6379 -a "${REDIS_PASS}" ping >/dev/null 2>&1; then
|
||||||
|
log "[✓] Redis erreichbar und Passwort akzeptiert."
|
||||||
|
else
|
||||||
|
log "[!] Warnung: Redis antwortet nicht oder Passwort falsch."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if redis-cli -h 127.0.0.1 -p 6379 ping >/dev/null 2>&1; then
|
||||||
|
log "[✓] Redis erreichbar (ohne Passwort)."
|
||||||
|
else
|
||||||
|
log "[!] Warnung: Redis antwortet nicht."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
systemctl enable --now rspamd || true
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────
|
||||||
|
# OpenDKIM – nur wenn DKIM_ENABLE=1
|
||||||
|
# ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if [[ "${DKIM_ENABLE}" != "1" ]]; then
|
||||||
|
log "DKIM_ENABLE=0 → OpenDKIM wird übersprungen."
|
||||||
|
/usr/sbin/postconf -e "milter_default_action = accept"
|
||||||
|
/usr/sbin/postconf -e "milter_protocol = 6"
|
||||||
|
/usr/sbin/postconf -e "smtpd_milters = inet:127.0.0.1:11332"
|
||||||
|
/usr/sbin/postconf -e "non_smtpd_milters = inet:127.0.0.1:11332"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
install -d -m 0755 /etc/opendkim
|
||||||
|
install -d -m 0750 /etc/opendkim/keys
|
||||||
|
chown -R opendkim:opendkim /etc/opendkim
|
||||||
|
chmod 750 /etc/opendkim/keys
|
||||||
|
|
||||||
|
# TrustedHosts
|
||||||
|
cat >/etc/opendkim/TrustedHosts <<'CONF'
|
||||||
|
127.0.0.1
|
||||||
|
::1
|
||||||
|
localhost
|
||||||
|
CONF
|
||||||
|
chown opendkim:opendkim /etc/opendkim/TrustedHosts
|
||||||
|
chmod 640 /etc/opendkim/TrustedHosts
|
||||||
|
|
||||||
|
# ── Key-Verzeichnis für SYSMAIL_DOMAIN vorbereiten ───────────────────────────
|
||||||
|
KEY_DIR="/etc/opendkim/keys/${SYSMAIL_DOMAIN}"
|
||||||
|
KEY_PRIV="${KEY_DIR}/${DKIM_SELECTOR}.private"
|
||||||
|
KEY_DNSTXT="${KEY_DIR}/${DKIM_SELECTOR}.txt"
|
||||||
|
install -d -m 0750 -o opendkim -g opendkim "${KEY_DIR}"
|
||||||
|
|
||||||
|
# ── Key optional generieren (nur wenn gewünscht) ─────────────────────────────
|
||||||
|
if [[ ! -s "${KEY_PRIV}" && "${DKIM_GENERATE}" = "1" ]]; then
|
||||||
|
if command -v opendkim-genkey >/dev/null 2>&1; then
|
||||||
|
opendkim-genkey -b 2048 -s "${DKIM_SELECTOR}" -d "${SYSMAIL_DOMAIN}" -D "${KEY_DIR}"
|
||||||
|
chown opendkim:opendkim "${KEY_DIR}/${DKIM_SELECTOR}.private" || true
|
||||||
|
chmod 600 "${KEY_DIR}/${DKIM_SELECTOR}.private" || true
|
||||||
|
else
|
||||||
|
echo "[!] opendkim-genkey fehlt – kann DKIM-Key nicht generieren."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Key-/SigningTable nur anlegen, nicht leeren ───────────────────────────────
|
||||||
|
touch /etc/opendkim/KeyTable /etc/opendkim/SigningTable
|
||||||
|
chown opendkim:opendkim /etc/opendkim/KeyTable /etc/opendkim/SigningTable
|
||||||
|
chmod 640 /etc/opendkim/KeyTable /etc/opendkim/SigningTable
|
||||||
|
|
||||||
|
if [[ -s "${KEY_PRIV}" && "${BASE_DOMAIN}" != "example.com" ]]; then
|
||||||
|
LINE_KT="${DKIM_SELECTOR}._domainkey.${SYSMAIL_DOMAIN} ${SYSMAIL_DOMAIN}:${DKIM_SELECTOR}:${KEY_PRIV}"
|
||||||
|
LINE_ST="*@${SYSMAIL_DOMAIN} ${DKIM_SELECTOR}._domainkey.${SYSMAIL_DOMAIN}"
|
||||||
|
grep -Fqx "$LINE_KT" /etc/opendkim/KeyTable || echo "$LINE_KT" >> /etc/opendkim/KeyTable
|
||||||
|
grep -Fqx "$LINE_ST" /etc/opendkim/SigningTable || echo "$LINE_ST" >> /etc/opendkim/SigningTable
|
||||||
|
else
|
||||||
|
echo "[i] Kein Private Key unter ${KEY_PRIV} – App-Helper trägt später ein."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Hauptkonfiguration ───────────────────────────────────────────────────────
|
||||||
|
cat >/etc/opendkim.conf <<'CONF'
|
||||||
|
Syslog yes
|
||||||
|
UMask 002
|
||||||
|
Mode sv
|
||||||
|
Socket inet:8891@127.0.0.1
|
||||||
|
PidFile /run/opendkim/opendkim.pid
|
||||||
|
Canonicalization relaxed/simple
|
||||||
|
|
||||||
|
On-BadSignature accept
|
||||||
|
On-Default accept
|
||||||
|
On-KeyNotFound accept
|
||||||
|
On-NoSignature accept
|
||||||
|
|
||||||
|
LogWhy yes
|
||||||
|
OversignHeaders From
|
||||||
|
|
||||||
|
KeyTable /etc/opendkim/KeyTable
|
||||||
|
SigningTable refile:/etc/opendkim/SigningTable
|
||||||
|
ExternalIgnoreList /etc/opendkim/TrustedHosts
|
||||||
|
InternalHosts /etc/opendkim/TrustedHosts
|
||||||
|
|
||||||
|
UserID opendkim:opendkim
|
||||||
|
AutoRestart yes
|
||||||
|
AutoRestartRate 10/1h
|
||||||
|
Background yes
|
||||||
|
DNSTimeout 5
|
||||||
|
SignatureAlgorithm rsa-sha256
|
||||||
|
SyslogSuccess yes
|
||||||
|
CONF
|
||||||
|
|
||||||
|
# ── systemd Drop-in: /run/opendkim sicherstellen ─────────────────────────────
|
||||||
|
install -d -m 0755 /etc/systemd/system/opendkim.service.d
|
||||||
|
cat >/etc/systemd/system/opendkim.service.d/override.conf <<'EOF'
|
||||||
|
[Service]
|
||||||
|
RuntimeDirectory=opendkim
|
||||||
|
RuntimeDirectoryMode=0755
|
||||||
|
EOF
|
||||||
|
|
||||||
|
install -d -o opendkim -g opendkim -m 0755 /run/opendkim
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────
|
||||||
|
# Root-Helper: DKIM installieren / entfernen + sudoers-Regel
|
||||||
|
# ──────────────────────────────────────────────────────────────
|
||||||
|
install -d -m 0750 /usr/local/sbin
|
||||||
|
|
||||||
|
# --- mailwolt-install-dkim ------------------------------------
|
||||||
|
cat > /usr/local/sbin/mailwolt-install-dkim <<'EOSH'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DOMAIN="$1"
|
||||||
|
SELECTOR="$2"
|
||||||
|
SRC_PRIV="$3"
|
||||||
|
SRC_TXT="${4:-}"
|
||||||
|
|
||||||
|
OKDIR="/etc/opendkim"
|
||||||
|
KEYDIR="${OKDIR}/keys/${DOMAIN}"
|
||||||
|
KEYPRI="${KEYDIR}/${SELECTOR}.private"
|
||||||
|
|
||||||
|
install -d -m 0750 -o opendkim -g opendkim "${KEYDIR}"
|
||||||
|
install -m 0600 -o opendkim -g opendkim "${SRC_PRIV}" "${KEYPRI}"
|
||||||
|
|
||||||
|
KT="${OKDIR}/KeyTable"
|
||||||
|
ST="${OKDIR}/SigningTable"
|
||||||
|
touch "$KT" "$ST"
|
||||||
|
chown opendkim:opendkim "$KT" "$ST"
|
||||||
|
chmod 0640 "$KT" "$ST"
|
||||||
|
|
||||||
|
LINE_KT="${SELECTOR}._domainkey.${DOMAIN} ${DOMAIN}:${SELECTOR}:${KEYPRI}"
|
||||||
|
LINE_ST="*@${DOMAIN} ${SELECTOR}._domainkey.${DOMAIN}"
|
||||||
|
|
||||||
|
grep -Fqx "$LINE_KT" "$KT" || echo "$LINE_KT" >> "$KT"
|
||||||
|
grep -Fqx "$LINE_ST" "$ST" || echo "$LINE_ST" >> "$ST"
|
||||||
|
|
||||||
|
if [[ -n "${SRC_TXT}" && -s "${SRC_TXT}" ]]; then
|
||||||
|
install -d -m 0755 /etc/mailwolt/dns
|
||||||
|
cp -f "${SRC_TXT}" "/etc/mailwolt/dns/dkim-${DOMAIN}.txt"
|
||||||
|
fi
|
||||||
|
|
||||||
|
systemctl is-active --quiet opendkim && systemctl reload opendkim || true
|
||||||
|
echo "OK"
|
||||||
|
EOSH
|
||||||
|
chmod 0750 /usr/local/sbin/mailwolt-install-dkim
|
||||||
|
chown root:root /usr/local/sbin/mailwolt-install-dkim
|
||||||
|
|
||||||
|
# --- 2) mailwolt-remove-dkim ----------------------------------
|
||||||
|
cat >/usr/local/sbin/mailwolt-remove-dkim <<'EOSH'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DOMAIN="$1" # z.B. kunden.tld oder sysmail.example.com
|
||||||
|
SELECTOR="$2" # z.B. mwl1
|
||||||
|
|
||||||
|
OKDIR="/etc/opendkim"
|
||||||
|
KEYDIR="${OKDIR}/keys/${DOMAIN}"
|
||||||
|
KEYPRI="${KEYDIR}/${SELECTOR}.private"
|
||||||
|
KT="${OKDIR}/KeyTable"
|
||||||
|
ST="${OKDIR}/SigningTable"
|
||||||
|
|
||||||
|
# Key-Datei löschen (falls vorhanden)
|
||||||
|
[[ -f "${KEYPRI}" ]] && rm -f "${KEYPRI}"
|
||||||
|
|
||||||
|
# Zeilen aus KeyTable und SigningTable entfernen
|
||||||
|
if [[ -f "$KT" ]]; then
|
||||||
|
tmp="$(mktemp)"; grep -v -F "${SELECTOR}._domainkey.${DOMAIN} ${DOMAIN}:${SELECTOR}:" "$KT" >"$tmp" && mv "$tmp" "$KT"
|
||||||
|
chown opendkim:opendkim "$KT"; chmod 0640 "$KT"
|
||||||
|
fi
|
||||||
|
if [[ -f "$ST" ]]; then
|
||||||
|
tmp="$(mktemp)"; grep -v -F "*@${DOMAIN} ${SELECTOR}._domainkey.${DOMAIN}" "$ST" >"$tmp" && mv "$tmp" "$ST"
|
||||||
|
chown opendkim:opendkim "$ST"; chmod 0640 "$ST"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verzeichnis ggf. aufräumen
|
||||||
|
rmdir "${KEYDIR}" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Dienst neu laden, falls aktiv
|
||||||
|
if systemctl is-active --quiet opendkim; then
|
||||||
|
systemctl reload opendkim || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "OK"
|
||||||
|
EOSH
|
||||||
|
chown root:root /usr/local/sbin/mailwolt-remove-dkim
|
||||||
|
chmod 0750 /usr/local/sbin/mailwolt-remove-dkim
|
||||||
|
|
||||||
|
# ── Dienst + Postfix-Milter aktivieren ─────────────────────────
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable opendkim || true
|
||||||
|
|
||||||
|
touch /run/mailwolt.need-apply-milters || true
|
||||||
|
|
||||||
|
chgrp _rspamd /etc/rspamd/local.d/*.inc /etc/rspamd/local.d/*.conf || true
|
||||||
|
chmod 0640 /etc/rspamd/local.d/*.inc /etc/rspamd/local.d/*.conf || true
|
||||||
|
|
||||||
|
#/usr/sbin/postconf -e "smtpd_milters = inet:127.0.0.1:11332, inet:127.0.0.1:8891"
|
||||||
|
#/usr/sbin/postconf -e "non_smtpd_milters = inet:127.0.0.1:11332, inet:127.0.0.1:8891"
|
||||||
|
|
||||||
|
log "[✓] Rspamd + OpenDKIM eingerichtet (läuft; signiert, sobald Keys vorhanden sind)."
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
log "OpenDMARC installieren/konfigurieren …"
|
||||||
|
|
||||||
|
# Flags laden
|
||||||
|
set +u
|
||||||
|
[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
|
||||||
|
set -u
|
||||||
|
OPENDMARC_ENABLE="${OPENDMARC_ENABLE:-1}"
|
||||||
|
|
||||||
|
# Paket sicherstellen
|
||||||
|
if ! dpkg -s opendmarc >/dev/null 2>&1; then
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y opendmarc
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Config-Verzeichnisse
|
||||||
|
install -d -m 0755 /etc/opendmarc
|
||||||
|
install -d -m 0755 /run/opendmarc
|
||||||
|
|
||||||
|
# IgnoreHosts
|
||||||
|
cat >/etc/opendmarc/ignore.hosts <<'EOF'
|
||||||
|
127.0.0.1
|
||||||
|
::1
|
||||||
|
localhost
|
||||||
|
EOF
|
||||||
|
chmod 0644 /etc/opendmarc/ignore.hosts
|
||||||
|
|
||||||
|
# Hauptkonfiguration
|
||||||
|
cat >/etc/opendmarc.conf <<'EOF'
|
||||||
|
AuthservID mailwolt
|
||||||
|
TrustedAuthservIDs mailwolt
|
||||||
|
IgnoreHosts /etc/opendmarc/ignore.hosts
|
||||||
|
Syslog true
|
||||||
|
SoftwareHeader true
|
||||||
|
Socket local:/run/opendmarc/opendmarc.sock
|
||||||
|
RejectFailures false
|
||||||
|
EOF
|
||||||
|
chmod 0644 /etc/opendmarc.conf
|
||||||
|
|
||||||
|
# systemd Drop-in für RuntimeDirectory (robust nach Reboot)
|
||||||
|
install -d -m 0755 /etc/systemd/system/opendmarc.service.d
|
||||||
|
cat >/etc/systemd/system/opendmarc.service.d/override.conf <<'EOF'
|
||||||
|
[Service]
|
||||||
|
RuntimeDirectory=opendmarc
|
||||||
|
RuntimeDirectoryMode=0755
|
||||||
|
EOF
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
|
||||||
|
# Dienst nach Flag
|
||||||
|
if [[ "$OPENDMARC_ENABLE" = "1" ]]; then
|
||||||
|
systemctl enable --now opendmarc
|
||||||
|
else
|
||||||
|
systemctl disable --now opendmarc || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Postfix-Milter-Kette konsistent setzen (Rspamd + OpenDKIM + optional OpenDMARC)
|
||||||
|
touch /run/mailwolt.need-apply-milters || true
|
||||||
|
|
||||||
|
log "[✓] OpenDMARC (ENABLE=${OPENDMARC_ENABLE}) bereit."
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
log "ClamAV (clamav-daemon) installieren/konfigurieren …"
|
||||||
|
|
||||||
|
# Flags laden
|
||||||
|
set +u
|
||||||
|
[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
|
||||||
|
set -u
|
||||||
|
CLAMAV_ENABLE="${CLAMAV_ENABLE:-0}"
|
||||||
|
|
||||||
|
# Pakete
|
||||||
|
if ! dpkg -s clamav-daemon >/dev/null 2>&1; then
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y clamav clamav-daemon
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Signaturen aktualisieren (erst Freshclam starten)
|
||||||
|
systemctl stop clamav-freshclam 2>/dev/null || true
|
||||||
|
freshclam || true
|
||||||
|
systemctl start clamav-freshclam || true
|
||||||
|
|
||||||
|
# clamd LocalSocket setzen
|
||||||
|
sed -i 's|^#\?LocalSocket .*|LocalSocket /run/clamav/clamd.ctl|' /etc/clamav/clamd.conf || true
|
||||||
|
install -d -m 0755 /run/clamav
|
||||||
|
chown clamav:clamav /run/clamav
|
||||||
|
|
||||||
|
# Dienst nach Flag
|
||||||
|
if [[ "$CLAMAV_ENABLE" = "1" ]]; then
|
||||||
|
systemctl enable --now clamav-daemon
|
||||||
|
else
|
||||||
|
systemctl disable --now clamav-daemon || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Rspamd-Integration (nur wenn aktiv)
|
||||||
|
AV_CONF="/etc/rspamd/local.d/antivirus.conf"
|
||||||
|
if [[ "$CLAMAV_ENABLE" = "1" ]]; then
|
||||||
|
cat >"$AV_CONF" <<'EOF'
|
||||||
|
clamav {
|
||||||
|
symbol = "CLAM_VIRUS";
|
||||||
|
type = "clamav";
|
||||||
|
servers = "/run/clamav/clamd.ctl";
|
||||||
|
scan_mime_parts = true;
|
||||||
|
scan_text_mime = true;
|
||||||
|
max_size = 50mb;
|
||||||
|
log_clean = false;
|
||||||
|
action = "reject";
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
chown root:_rspamd "$AV_CONF" || true
|
||||||
|
chmod 0640 "$AV_CONF" || true
|
||||||
|
systemctl reload rspamd || systemctl restart rspamd
|
||||||
|
else
|
||||||
|
rm -f "$AV_CONF" || true
|
||||||
|
systemctl reload rspamd || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "[✓] ClamAV (ENABLE=${CLAMAV_ENABLE}) konfiguriert."
|
||||||
|
|
@ -0,0 +1,230 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
log "Fail2Ban installieren/konfigurieren …"
|
||||||
|
|
||||||
|
# Flags laden
|
||||||
|
set +u
|
||||||
|
[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
|
||||||
|
set -u
|
||||||
|
FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}"
|
||||||
|
|
||||||
|
# Paket
|
||||||
|
if ! dpkg -s fail2ban >/dev/null 2>&1; then
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y fail2ban sqlite3
|
||||||
|
fi
|
||||||
|
|
||||||
|
install -d -m 0755 /etc/fail2ban/jail.d
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
# Basis-Jails (praxisnah)
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
cat >/etc/fail2ban/jail.d/mailwolt.conf <<'EOF'
|
||||||
|
[sshd]
|
||||||
|
enabled = true
|
||||||
|
port = ssh
|
||||||
|
logpath = /var/log/auth.log
|
||||||
|
|
||||||
|
[postfix]
|
||||||
|
enabled = true
|
||||||
|
logpath = /var/log/mail.log
|
||||||
|
port = smtp,ssmtp,submission,465
|
||||||
|
|
||||||
|
[dovecot]
|
||||||
|
enabled = true
|
||||||
|
logpath = /var/log/mail.log
|
||||||
|
port = pop3,pop3s,imap,imaps,submission,465,587,993
|
||||||
|
|
||||||
|
[rspamd-controller]
|
||||||
|
enabled = true
|
||||||
|
port = 11334
|
||||||
|
filter = rspamd
|
||||||
|
logpath = /var/log/rspamd/rspamd.log
|
||||||
|
maxretry = 5
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# einfacher Filter für Rspamd-Controller
|
||||||
|
if [ ! -f /etc/fail2ban/filter.d/rspamd.conf ]; then
|
||||||
|
cat >/etc/fail2ban/filter.d/rspamd.conf <<'EOF'
|
||||||
|
[Definition]
|
||||||
|
failregex = .*Authentication failed for user.* from <HOST>
|
||||||
|
ignoreregex =
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
# Fail2Ban-Backend auf SQLite umstellen
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
log "SQLite-Backend aktivieren …"
|
||||||
|
|
||||||
|
cat >/etc/fail2ban/fail2ban.local <<'EOF'
|
||||||
|
[Definition]
|
||||||
|
loglevel = INFO
|
||||||
|
logtarget = /var/log/fail2ban.log
|
||||||
|
dbfile = /var/lib/fail2ban/fail2ban.sqlite3
|
||||||
|
dbpurgeage = 86400
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Datenbankverzeichnis sicherstellen
|
||||||
|
install -d -o fail2ban -g fail2ban -m 0750 /var/lib/fail2ban
|
||||||
|
|
||||||
|
# Falls DB nicht existiert, Dummy anlegen (wird vom Dienst erweitert)
|
||||||
|
if [ ! -f /var/lib/fail2ban/fail2ban.sqlite3 ]; then
|
||||||
|
sqlite3 /var/lib/fail2ban/fail2ban.sqlite3 "VACUUM;"
|
||||||
|
fi
|
||||||
|
chown fail2ban:fail2ban /var/lib/fail2ban/fail2ban.sqlite3
|
||||||
|
chmod 0640 /var/lib/fail2ban/fail2ban.sqlite3
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
# sudoers für Web-UI
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
# Fail2Ban Blacklist-Jail
|
||||||
|
cat >/etc/fail2ban/jail.d/mailwolt-blacklist.local <<'EOF'
|
||||||
|
[mailwolt-blacklist]
|
||||||
|
enabled = true
|
||||||
|
filter = none
|
||||||
|
port = anyport
|
||||||
|
bantime = -1
|
||||||
|
findtime = 1
|
||||||
|
maxretry = 1
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat >/etc/fail2ban/filter.d/none.conf <<'EOF'
|
||||||
|
[Definition]
|
||||||
|
failregex =
|
||||||
|
ignoreregex =
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod 0640 /etc/fail2ban/filter.d/none.conf
|
||||||
|
|
||||||
|
|
||||||
|
SUDOERS_F2B="/etc/sudoers.d/mailwolt-fail2ban"
|
||||||
|
cat > "${SUDOERS_F2B}" <<'EOF'
|
||||||
|
Defaults:www-data !requiretty
|
||||||
|
www-data ALL=(root) NOPASSWD: \
|
||||||
|
/usr/bin/fail2ban-client, \
|
||||||
|
/usr/bin/fail2ban-client ping, \
|
||||||
|
/usr/bin/fail2ban-client status, \
|
||||||
|
/usr/bin/fail2ban-client status *, \
|
||||||
|
/usr/bin/fail2ban-client get *, \
|
||||||
|
/usr/bin/fail2ban-client set * banip *, \
|
||||||
|
/usr/bin/fail2ban-client set * unbanip *, \
|
||||||
|
/usr/bin/fail2ban-client reload, \
|
||||||
|
/usr/bin/journalctl, \
|
||||||
|
/bin/journalctl, \
|
||||||
|
/usr/bin/zgrep, \
|
||||||
|
/bin/zgrep, \
|
||||||
|
/usr/bin/grep, \
|
||||||
|
/bin/grep, \
|
||||||
|
/usr/bin/tail, \
|
||||||
|
/bin/tail, \
|
||||||
|
/usr/bin/sqlite3, \
|
||||||
|
/usr/bin/tee /etc/fail2ban/jail.d/*
|
||||||
|
EOF
|
||||||
|
chown root:root "${SUDOERS_F2B}"
|
||||||
|
chmod 440 "${SUDOERS_F2B}"
|
||||||
|
|
||||||
|
if ! visudo -c -f "${SUDOERS_F2B}" >/dev/null 2>&1; then
|
||||||
|
echo "[!] Ungültiger sudoers-Eintrag in ${SUDOERS_F2B} – entferne Datei."
|
||||||
|
rm -f "${SUDOERS_F2B}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
# Dienst aktivieren/deaktivieren
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
if [[ "$FAIL2BAN_ENABLE" = "1" ]]; then
|
||||||
|
systemctl enable --now fail2ban
|
||||||
|
else
|
||||||
|
systemctl disable --now fail2ban || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "[✓] Fail2Ban (ENABLE=${FAIL2BAN_ENABLE}) bereit."
|
||||||
|
|
||||||
|
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#set -euo pipefail
|
||||||
|
#source ./lib.sh
|
||||||
|
#
|
||||||
|
#log "Fail2Ban installieren/konfigurieren …"
|
||||||
|
#
|
||||||
|
## Flags laden
|
||||||
|
#set +u
|
||||||
|
#[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
|
||||||
|
#set -u
|
||||||
|
#FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}"
|
||||||
|
#
|
||||||
|
## Paket
|
||||||
|
#if ! dpkg -s fail2ban >/dev/null 2>&1; then
|
||||||
|
# apt-get update -qq
|
||||||
|
# apt-get install -y fail2ban
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
#install -d -m 0755 /etc/fail2ban/jail.d
|
||||||
|
#
|
||||||
|
## Basis-Jails (praxisnah)
|
||||||
|
#cat >/etc/fail2ban/jail.d/mailwolt.conf <<'EOF'
|
||||||
|
#[DEFAULT]
|
||||||
|
#bantime = 1h
|
||||||
|
#findtime = 10m
|
||||||
|
#maxretry = 5
|
||||||
|
#backend = auto
|
||||||
|
#
|
||||||
|
#[sshd]
|
||||||
|
#enabled = true
|
||||||
|
#port = ssh
|
||||||
|
#logpath = /var/log/auth.log
|
||||||
|
#
|
||||||
|
#[postfix]
|
||||||
|
#enabled = true
|
||||||
|
#logpath = /var/log/mail.log
|
||||||
|
#port = smtp,ssmtp,submission,465
|
||||||
|
#
|
||||||
|
#[dovecot]
|
||||||
|
#enabled = true
|
||||||
|
#logpath = /var/log/mail.log
|
||||||
|
#port = pop3,pop3s,imap,imaps,submission,465,587,993
|
||||||
|
#
|
||||||
|
#[rspamd-controller]
|
||||||
|
#enabled = true
|
||||||
|
#port = 11334
|
||||||
|
#filter = rspamd
|
||||||
|
#logpath = /var/log/rspamd/rspamd.log
|
||||||
|
#maxretry = 5
|
||||||
|
#EOF
|
||||||
|
#
|
||||||
|
## einfacher Filter für Rspamd-Controller
|
||||||
|
#if [ ! -f /etc/fail2ban/filter.d/rspamd.conf ]; then
|
||||||
|
# cat >/etc/fail2ban/filter.d/rspamd.conf <<'EOF'
|
||||||
|
#[Definition]
|
||||||
|
#failregex = .*Authentication failed for user.* from <HOST>
|
||||||
|
#ignoreregex =
|
||||||
|
#EOF
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
#SUDOERS_F2B="/etc/sudoers.d/mailwolt-fail2ban"
|
||||||
|
#cat > "${SUDOERS_F2B}" <<'EOF'
|
||||||
|
#www-data ALL=(root) NOPASSWD: /usr/bin/fail2ban-client status, /usr/bin/fail2ban-client status *
|
||||||
|
#EOF
|
||||||
|
#chown root:root "${SUDOERS_F2B}"
|
||||||
|
#chmod 440 "${SUDOERS_F2B}"
|
||||||
|
#
|
||||||
|
#if ! visudo -c -f "${SUDOERS_F2B}" >/dev/null 2>&1; then
|
||||||
|
# echo "[!] Ungültiger sudoers-Eintrag in ${SUDOERS_F2B} – entferne Datei."
|
||||||
|
# rm -f "${SUDOERS_F2B}"
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
#sudo tee /etc/sudoers.d/mailwolt-fail2ban >/dev/null <<'EOF'
|
||||||
|
#www-data ALL=(root) NOPASSWD: /usr/bin/fail2ban-client status, /usr/bin/fail2ban-client status *
|
||||||
|
#EOF
|
||||||
|
#sudo visudo -cf /etc/sudoers.d/mailwolt-fail2ban
|
||||||
|
#
|
||||||
|
## Dienst nach Flag
|
||||||
|
#if [[ "$FAIL2BAN_ENABLE" = "1" ]]; then
|
||||||
|
# systemctl enable --now fail2ban
|
||||||
|
#else
|
||||||
|
# systemctl disable --now fail2ban || true
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
#log "[✓] Fail2Ban (ENABLE=${FAIL2BAN_ENABLE}) bereit."
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
# nur ausführen, wenn vorherige Schritte das Flag gesetzt haben
|
||||||
|
if [[ -f /run/mailwolt.need-apply-milters ]]; then
|
||||||
|
if command -v /usr/local/sbin/mailwolt-apply-milters >/dev/null 2>&1; then
|
||||||
|
log "Setze Postfix-Milter-Kette (Rspamd/OpenDKIM[/OpenDMARC]) …"
|
||||||
|
/usr/local/sbin/mailwolt-apply-milters || true
|
||||||
|
else
|
||||||
|
# Fallback (ident wie im Tool)
|
||||||
|
/usr/sbin/postconf -e "milter_default_action = accept"
|
||||||
|
/usr/sbin/postconf -e "milter_protocol = 6"
|
||||||
|
CHAIN="inet:127.0.0.1:11333, inet:127.0.0.1:8891"
|
||||||
|
systemctl is-active --quiet opendmarc && CHAIN="$CHAIN, inet:127.0.0.1:8893" || true
|
||||||
|
/usr/sbin/postconf -e "smtpd_milters = $CHAIN"
|
||||||
|
/usr/sbin/postconf -e "non_smtpd_milters = $CHAIN"
|
||||||
|
systemctl reload postfix || true
|
||||||
|
fi
|
||||||
|
rm -f /run/mailwolt.need-apply-milters || true
|
||||||
|
log "[✓] Milter-Kette angewandt."
|
||||||
|
else
|
||||||
|
log "Milter-Kette: kein Bedarf (Flag nicht gesetzt) – überspringe."
|
||||||
|
fi
|
||||||
|
|
@ -0,0 +1,528 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
log "Nginx konfigurieren …"
|
||||||
|
|
||||||
|
# ── Flags/Umgebung (vom Bootstrap gesetzt; hier Fallbacks) ────────────────
|
||||||
|
DEV_MODE="${DEV_MODE:-0}" # 1 = DEV (Vite-Proxy aktiv), 0 = PROD
|
||||||
|
PROXY_MODE="${PROXY_MODE:-0}" # 1 = NPM/Proxy davor, Backend spricht nur HTTP:80
|
||||||
|
NPM_IP="${NPM_IP:-}" # z.B. 10.10.20.20
|
||||||
|
|
||||||
|
# Erwartet vom Bootstrap/Installer exportiert:
|
||||||
|
: "${UI_HOST:?UI_HOST fehlt}"
|
||||||
|
: "${WEBMAIL_HOST:?WEBMAIL_HOST fehlt}"
|
||||||
|
: "${APP_DIR:?APP_DIR fehlt}"
|
||||||
|
|
||||||
|
ACME_ROOT="/var/www/letsencrypt"
|
||||||
|
install -d -m 0755 "$ACME_ROOT"
|
||||||
|
|
||||||
|
# Default-Sites entfernen (verhindert doppelten default_server)
|
||||||
|
rm -f /etc/nginx/sites-enabled/default /etc/nginx/sites-available/default || true
|
||||||
|
|
||||||
|
# HTTP/2-Unterstützung erkennen
|
||||||
|
NGINX_HTTP2_SUFFIX=""
|
||||||
|
if nginx -V 2>&1 | grep -q http_v2; then
|
||||||
|
NGINX_HTTP2_SUFFIX=" http2"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# PHP-FPM Socket/TCP finden → 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};"
|
||||||
|
else
|
||||||
|
FASTCGI_PASS="fastcgi_pass ${PHP_FPM_TARGET};"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Builder 1: HTTP-only (Proxy-Mode: TLS endet im NPM) ───────────────────
|
||||||
|
## $1=host, $2=outfile
|
||||||
|
#build_site_http_only(){
|
||||||
|
# local host="$1" outfile="$2"
|
||||||
|
#
|
||||||
|
# local def=""
|
||||||
|
# [[ "${DEV_MODE}" = "1" ]] && def=" default_server"
|
||||||
|
# [[ -z "${host}" || "${host}" = "_" ]] && host="_"
|
||||||
|
#
|
||||||
|
# cat > "$outfile" <<CONF
|
||||||
|
## --- ${host} : HTTP (kein Redirect, kein TLS; läuft hinter Reverse-Proxy) ---
|
||||||
|
#server {
|
||||||
|
# listen 80;
|
||||||
|
# listen [::]:80;
|
||||||
|
# server_name ${host};
|
||||||
|
#
|
||||||
|
# # ACME HTTP-01 (optional; meist übernimmt das der Proxy)
|
||||||
|
# location ^~ /.well-known/acme-challenge/ {
|
||||||
|
# root ${ACME_ROOT};
|
||||||
|
# allow all;
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# root ${APP_DIR}/public;
|
||||||
|
# index index.php index.html;
|
||||||
|
#
|
||||||
|
# access_log /var/log/nginx/${host}_access.log;
|
||||||
|
# error_log /var/log/nginx/${host}_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}
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# 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 (Backend intern HTTP)
|
||||||
|
# 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/;
|
||||||
|
# }
|
||||||
|
#CONF
|
||||||
|
#
|
||||||
|
# if [[ "${DEV_MODE}" = "1" ]]; then
|
||||||
|
# cat >> "$outfile" <<'CONF'
|
||||||
|
# # DEV: Vite-Proxy (HMR)
|
||||||
|
# location ^~ /@vite/ { proxy_pass http://127.0.0.1:5173/@vite/; proxy_set_header Host $host; }
|
||||||
|
# location ^~ /node_modules/ { proxy_pass http://127.0.0.1:5173/node_modules/; proxy_set_header Host $host; }
|
||||||
|
# location ^~ /resources/ { proxy_pass http://127.0.0.1:5173/resources/; proxy_set_header Host $host; }
|
||||||
|
#CONF
|
||||||
|
# fi
|
||||||
|
#
|
||||||
|
# echo "}" >> "$outfile"
|
||||||
|
#}
|
||||||
|
|
||||||
|
#build_site_http_only(){
|
||||||
|
# local host="$1" outfile="$2"
|
||||||
|
#
|
||||||
|
# # DEV: IP-Zugriff ohne Hostname → default_server + server_name _
|
||||||
|
# local def=""
|
||||||
|
# if [[ "${DEV_MODE}" = "1" ]]; then
|
||||||
|
# def=" default_server"
|
||||||
|
# host="_"
|
||||||
|
# fi
|
||||||
|
# [[ -z "${host}" || "${host}" = "_" ]] && host="_"
|
||||||
|
#
|
||||||
|
# cat > "$outfile" <<CONF
|
||||||
|
## --- ${host} : HTTP (kein Redirect, kein TLS; läuft hinter Reverse-Proxy/DEV) ---
|
||||||
|
#server {
|
||||||
|
# listen 80${def};
|
||||||
|
# listen [::]:80${def};
|
||||||
|
# server_name ${host};
|
||||||
|
#
|
||||||
|
# # ACME HTTP-01 (optional; meist übernimmt das der Proxy)
|
||||||
|
# location ^~ /.well-known/acme-challenge/ {
|
||||||
|
# root ${ACME_ROOT};
|
||||||
|
# allow all;
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# root ${APP_DIR}/public;
|
||||||
|
# index index.php index.html;
|
||||||
|
#
|
||||||
|
# access_log /var/log/nginx/${host/_/__}_access.log;
|
||||||
|
# error_log /var/log/nginx/${host/_/__}_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}
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# 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/;
|
||||||
|
# }
|
||||||
|
#CONF
|
||||||
|
#
|
||||||
|
# if [[ "${DEV_MODE}" = "1" ]]; then
|
||||||
|
# cat >> "$outfile" <<'CONF'
|
||||||
|
# # DEV: Vite-Proxy (HMR)
|
||||||
|
# location ^~ /@vite/ { proxy_pass http://127.0.0.1:5173/@vite/; proxy_set_header Host $host; }
|
||||||
|
# location ^~ /node_modules/ { proxy_pass http://127.0.0.1:5173/node_modules/; proxy_set_header Host $host; }
|
||||||
|
# location ^~ /resources/ { proxy_pass http://127.0.0.1:5173/resources/; proxy_set_header Host $host; }
|
||||||
|
#CONF
|
||||||
|
# fi
|
||||||
|
#
|
||||||
|
# echo "}" >> "$outfile"
|
||||||
|
#}
|
||||||
|
|
||||||
|
# $1=host, $2=outfile, $3=default_flag (default|nodefault)
|
||||||
|
build_site_http_only(){
|
||||||
|
local host="$1" outfile="$2" def_flag="${3:-default}"
|
||||||
|
|
||||||
|
local def=""
|
||||||
|
if [[ "${DEV_MODE}" = "1" && "${def_flag}" = "default" ]]; then
|
||||||
|
def=" default_server"
|
||||||
|
fi
|
||||||
|
[[ -z "${host}" || "${host}" = "_" ]] && host="_"
|
||||||
|
|
||||||
|
cat > "$outfile" <<CONF
|
||||||
|
# --- ${host} : HTTP (kein Redirect, kein TLS; läuft hinter Reverse-Proxy/DEV) ---
|
||||||
|
server {
|
||||||
|
listen 80${def};
|
||||||
|
listen [::]:80${def};
|
||||||
|
server_name ${host};
|
||||||
|
|
||||||
|
location ^~ /.well-known/acme-challenge/ {
|
||||||
|
root ${ACME_ROOT};
|
||||||
|
allow all;
|
||||||
|
}
|
||||||
|
|
||||||
|
root ${APP_DIR}/public;
|
||||||
|
index index.php index.html;
|
||||||
|
|
||||||
|
access_log /var/log/nginx/${host/_/__}_access.log;
|
||||||
|
error_log /var/log/nginx/${host/_/__}_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}
|
||||||
|
}
|
||||||
|
|
||||||
|
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/;
|
||||||
|
}
|
||||||
|
CONF
|
||||||
|
|
||||||
|
if [[ "${DEV_MODE}" = "1" ]]; then
|
||||||
|
cat >> "$outfile" <<'CONF'
|
||||||
|
# DEV: Vite-Proxy (HMR)
|
||||||
|
location ^~ /@vite/ { proxy_pass http://127.0.0.1:5173/@vite/; proxy_set_header Host $host; }
|
||||||
|
location ^~ /node_modules/ { proxy_pass http://127.0.0.1:5173/node_modules/; proxy_set_header Host $host; }
|
||||||
|
location ^~ /resources/ { proxy_pass http://127.0.0.1:5173/resources/; proxy_set_header Host $host; }
|
||||||
|
CONF
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "}" >> "$outfile"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Builder 2: 80→443 Redirect + 443/TLS (Live-Server) ────────────────────
|
||||||
|
# $1=host, $2=cert_dir (/etc/ssl/ui | /etc/ssl/webmail), $3=outfile
|
||||||
|
build_site_tls(){
|
||||||
|
local host="$1" cert_dir="$2" outfile="$3"
|
||||||
|
local cert="${cert_dir}/fullchain.pem"
|
||||||
|
local key="${cert_dir}/privkey.pem"
|
||||||
|
|
||||||
|
cat > "$outfile" <<CONF
|
||||||
|
# --- ${host} : HTTP (ACME + Redirect) ---
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name ${host};
|
||||||
|
|
||||||
|
# ACME HTTP-01 auf Port 80
|
||||||
|
location ^~ /.well-known/acme-challenge/ {
|
||||||
|
root ${ACME_ROOT};
|
||||||
|
allow all;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 301 https://\$host\$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- ${host} : HTTPS ---
|
||||||
|
server {
|
||||||
|
listen 443 ssl${NGINX_HTTP2_SUFFIX};
|
||||||
|
listen [::]:443 ssl${NGINX_HTTP2_SUFFIX};
|
||||||
|
server_name ${host};
|
||||||
|
|
||||||
|
ssl_certificate ${cert};
|
||||||
|
ssl_certificate_key ${key};
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
|
||||||
|
# WICHTIG: ACME auch auf 443, sonst 404 bei Redirects
|
||||||
|
location ^~ /.well-known/acme-challenge/ {
|
||||||
|
root ${ACME_ROOT};
|
||||||
|
allow all;
|
||||||
|
}
|
||||||
|
|
||||||
|
root ${APP_DIR}/public;
|
||||||
|
index index.php index.html;
|
||||||
|
|
||||||
|
access_log /var/log/nginx/${host}_ssl_access.log;
|
||||||
|
error_log /var/log/nginx/${host}_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}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (Backend intern HTTP)
|
||||||
|
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/;
|
||||||
|
}
|
||||||
|
CONF
|
||||||
|
|
||||||
|
if [[ "${DEV_MODE}" = "1" ]]; then
|
||||||
|
cat >> "$outfile" <<'CONF'
|
||||||
|
# DEV: Vite-Proxy (npm run dev)
|
||||||
|
location = /vite-hmr {
|
||||||
|
proxy_pass http://127.0.0.1:5173/vite-hmr;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "Upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
}
|
||||||
|
location ^~ /@vite/ { proxy_pass http://127.0.0.1:5173/@vite/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto https; }
|
||||||
|
location ^~ /node_modules/ { proxy_pass http://127.0.0.1:5173/node_modules/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto https; }
|
||||||
|
location ^~ /resources/ { proxy_pass http://127.0.0.1:5173/resources/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto https; }
|
||||||
|
CONF
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "}" >> "$outfile"
|
||||||
|
}
|
||||||
|
|
||||||
|
build_site_acme_only(){
|
||||||
|
local host="$1" outfile="$2"
|
||||||
|
|
||||||
|
cat > "$outfile" <<CONF
|
||||||
|
# --- ${host} : ACME-only (80 + 443), KEIN App-Root ---
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name ${host};
|
||||||
|
|
||||||
|
# HTTP-01 Challenge exakt ausliefern
|
||||||
|
location ^~ /.well-known/acme-challenge/ {
|
||||||
|
root ${ACME_ROOT};
|
||||||
|
default_type "text/plain";
|
||||||
|
try_files \$uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Alles andere → nach https
|
||||||
|
location / { return 301 https://\$host\$request_uri; }
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl${NGINX_HTTP2_SUFFIX};
|
||||||
|
listen [::]:443 ssl${NGINX_HTTP2_SUFFIX};
|
||||||
|
server_name ${host};
|
||||||
|
|
||||||
|
ssl_certificate /etc/ssl/mail/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/ssl/mail/privkey.pem;
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
|
||||||
|
# Auch via https die Challenge bedienen (falls Redirects gefolgt werden)
|
||||||
|
location ^~ /.well-known/acme-challenge/ {
|
||||||
|
root ${ACME_ROOT};
|
||||||
|
default_type "text/plain";
|
||||||
|
try_files \$uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Sonst nichts preisgeben
|
||||||
|
location / { return 444; }
|
||||||
|
}
|
||||||
|
CONF
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Builder Webmail: nur /webmail/* erlaubt, root → redirect ───────────────
|
||||||
|
build_webmail_http_only(){
|
||||||
|
local host="$1" outfile="$2" def_flag="${3:-nodefault}"
|
||||||
|
local def=""
|
||||||
|
[[ "${DEV_MODE}" = "1" && "${def_flag}" = "default" ]] && def=" default_server"
|
||||||
|
[[ -z "${host}" || "${host}" = "_" ]] && host="_"
|
||||||
|
cat > "$outfile" <<CONF
|
||||||
|
# --- ${host} : Webmail (domain-routing, volle Laravel-App) ---
|
||||||
|
server {
|
||||||
|
listen 80${def};
|
||||||
|
listen [::]:80${def};
|
||||||
|
server_name ${host};
|
||||||
|
location ^~ /.well-known/acme-challenge/ { root ${ACME_ROOT}; allow all; }
|
||||||
|
root ${APP_DIR}/public;
|
||||||
|
index index.php;
|
||||||
|
access_log /var/log/nginx/${host/_/__}_webmail_access.log;
|
||||||
|
error_log /var/log/nginx/${host/_/__}_webmail_error.log;
|
||||||
|
client_max_body_size 25m;
|
||||||
|
location = / { return 301 http://\$host/login; }
|
||||||
|
location / { try_files \$uri \$uri/ /index.php?\$query_string; }
|
||||||
|
location ~ \.php\$ { include snippets/fastcgi-php.conf; ${FASTCGI_PASS} }
|
||||||
|
location ^~ /livewire/ { try_files \$uri /index.php?\$query_string; }
|
||||||
|
location ~* \.(css|js|ico|svg|woff2?|ttf|jpg|jpeg|png|gif)\$ { expires 30d; access_log off; }
|
||||||
|
}
|
||||||
|
CONF
|
||||||
|
}
|
||||||
|
|
||||||
|
build_webmail_tls(){
|
||||||
|
local host="$1" cert_dir="$2" outfile="$3"
|
||||||
|
cat > "$outfile" <<CONF
|
||||||
|
# --- ${host} : Webmail TLS (domain-routing, volle Laravel-App) ---
|
||||||
|
server {
|
||||||
|
listen 80; listen [::]:80; server_name ${host};
|
||||||
|
location ^~ /.well-known/acme-challenge/ { root ${ACME_ROOT}; allow all; }
|
||||||
|
return 301 https://\$host\$request_uri;
|
||||||
|
}
|
||||||
|
server {
|
||||||
|
listen 443 ssl${NGINX_HTTP2_SUFFIX}; listen [::]:443 ssl${NGINX_HTTP2_SUFFIX};
|
||||||
|
server_name ${host};
|
||||||
|
ssl_certificate ${cert_dir}/fullchain.pem;
|
||||||
|
ssl_certificate_key ${cert_dir}/privkey.pem;
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
location ^~ /.well-known/acme-challenge/ { root ${ACME_ROOT}; allow all; }
|
||||||
|
root ${APP_DIR}/public;
|
||||||
|
index index.php;
|
||||||
|
access_log /var/log/nginx/${host}_webmail_ssl_access.log;
|
||||||
|
error_log /var/log/nginx/${host}_webmail_ssl_error.log;
|
||||||
|
client_max_body_size 25m;
|
||||||
|
location = / { return 301 https://\$host/login; }
|
||||||
|
location / { try_files \$uri \$uri/ /index.php?\$query_string; }
|
||||||
|
location ~ \.php\$ { include snippets/fastcgi-php.conf; ${FASTCGI_PASS} }
|
||||||
|
location ^~ /livewire/ { try_files \$uri /index.php?\$query_string; }
|
||||||
|
location ~* \.(css|js|ico|svg|woff2?|ttf|jpg|jpeg|png|gif)\$ { expires 30d; access_log off; }
|
||||||
|
}
|
||||||
|
CONF
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Sites erzeugen ─────────────────────────────────────────────────────────
|
||||||
|
MX_SITE="/etc/nginx/sites-available/mx-mailwolt.conf"
|
||||||
|
UI_SITE="/etc/nginx/sites-available/ui-mailwolt.conf"
|
||||||
|
WEBMAIL_SITE="/etc/nginx/sites-available/webmail-mailwolt.conf"
|
||||||
|
|
||||||
|
# UI & Webmail wie gehabt …
|
||||||
|
#if [[ "${PROXY_MODE:-0}" -eq 1 ]]; then
|
||||||
|
# build_site_http_only "$UI_HOST" "$UI_SITE"
|
||||||
|
# build_site_http_only "$WEBMAIL_HOST" "$WEBMAIL_SITE"
|
||||||
|
#else
|
||||||
|
# build_site_tls "$UI_HOST" "/etc/ssl/ui" "$UI_SITE"
|
||||||
|
# build_site_tls "$WEBMAIL_HOST" "/etc/ssl/webmail" "$WEBMAIL_SITE"
|
||||||
|
#fi
|
||||||
|
|
||||||
|
# UI & Webmail …
|
||||||
|
if [[ "${DEV_MODE}" = "1" ]]; then
|
||||||
|
build_site_http_only "_" "$UI_SITE" "default"
|
||||||
|
build_webmail_http_only "_" "$WEBMAIL_SITE" "nodefault"
|
||||||
|
else
|
||||||
|
if [[ "${PROXY_MODE:-0}" -eq 1 ]]; then
|
||||||
|
build_site_http_only "$UI_HOST" "$UI_SITE"
|
||||||
|
build_webmail_http_only "$WEBMAIL_HOST" "$WEBMAIL_SITE"
|
||||||
|
else
|
||||||
|
build_site_tls "$UI_HOST" "/etc/ssl/ui" "$UI_SITE"
|
||||||
|
build_webmail_tls "$WEBMAIL_HOST" "/etc/ssl/webmail" "$WEBMAIL_SITE"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
#if [[ "${DEV_MODE}" = "1" ]]; then
|
||||||
|
# # DEV: per IP erreichbar → Catch-All („_“) und HTTP-only
|
||||||
|
# build_site_http_only "_" "$UI_SITE"
|
||||||
|
# build_site_http_only "_" "$WEBMAIL_SITE"
|
||||||
|
#else
|
||||||
|
# if [[ "${PROXY_MODE:-0}" -eq 1 ]]; then
|
||||||
|
# build_site_http_only "$UI_HOST" "$UI_SITE"
|
||||||
|
# build_site_http_only "$WEBMAIL_HOST" "$WEBMAIL_SITE"
|
||||||
|
# else
|
||||||
|
# build_site_tls "$UI_HOST" "/etc/ssl/ui" "$UI_SITE"
|
||||||
|
# build_site_tls "$WEBMAIL_HOST" "/etc/ssl/webmail" "$WEBMAIL_SITE"
|
||||||
|
# fi
|
||||||
|
#fi
|
||||||
|
|
||||||
|
# MX: **immer** ACME-only (kein Laravel dahinter)
|
||||||
|
build_site_acme_only "${MAIL_HOSTNAME}" "$MX_SITE"
|
||||||
|
|
||||||
|
ln -sf "$UI_SITE" /etc/nginx/sites-enabled/ui-mailwolt.conf
|
||||||
|
ln -sf "$WEBMAIL_SITE" /etc/nginx/sites-enabled/webmail-mailwolt.conf
|
||||||
|
ln -sf "$MX_SITE" /etc/nginx/sites-enabled/mx-mailwolt.conf
|
||||||
|
|
||||||
|
# ── Real-IP nur, wenn Proxy davor ──────────────────────────────────────────
|
||||||
|
if [[ "${PROXY_MODE}" -eq 1 && -n "${NPM_IP}" ]]; then
|
||||||
|
cat > /etc/nginx/conf.d/realip.conf <<NGX
|
||||||
|
real_ip_header X-Forwarded-For;
|
||||||
|
set_real_ip_from ${NPM_IP};
|
||||||
|
real_ip_recursive on;
|
||||||
|
NGX
|
||||||
|
else
|
||||||
|
rm -f /etc/nginx/conf.d/realip.conf || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 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
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
ACME_WEBROOT="/var/www/letsencrypt"
|
||||||
|
install -d -m 0755 "${ACME_WEBROOT}/.well-known/acme-challenge"
|
||||||
|
|
||||||
|
# Staging optional (verbraucht kein Live-Limit)
|
||||||
|
CERTBOT_EXTRA=()
|
||||||
|
LE_STAGING="${LE_STAGING:-0}"
|
||||||
|
[[ "$LE_STAGING" = "1" ]] && CERTBOT_EXTRA+=(--test-cert)
|
||||||
|
|
||||||
|
# Einheitliche LE-Mail (Fallback)
|
||||||
|
LE_MAIL="${LE_EMAIL:-admin@${BASE_DOMAIN}}"
|
||||||
|
|
||||||
|
resolve_ok() {
|
||||||
|
local host="$1"
|
||||||
|
local pats=()
|
||||||
|
[[ -n "${SERVER_PUBLIC_IPV4:-}" ]] && pats+=("${SERVER_PUBLIC_IPV4//./\\.}")
|
||||||
|
[[ -n "${SERVER_PUBLIC_IPV6:-}" ]] && pats+=("${SERVER_PUBLIC_IPV6//:/\\:}")
|
||||||
|
[[ ${#pats[@]} -eq 0 ]] && return 0
|
||||||
|
getent ahosts "$host" | awk '{print $1}' | sort -u \
|
||||||
|
| grep -Eq "^($(IFS='|'; echo "${pats[*]}"))$"
|
||||||
|
}
|
||||||
|
|
||||||
|
probe_http() {
|
||||||
|
local host="$1"
|
||||||
|
echo test > "${ACME_WEBROOT}/.well-known/acme-challenge/_probe"
|
||||||
|
curl -fsS --max-time 5 -4 "http://${host}/.well-known/acme-challenge/_probe" >/dev/null \
|
||||||
|
|| curl -fsS --max-time 5 -6 "http://${host}/.well-known/acme-challenge/_probe" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
issue() {
|
||||||
|
local host="${1:-}"
|
||||||
|
[[ -z "$host" ]] && return 0
|
||||||
|
|
||||||
|
echo "[i] Versuche LE für ${host} …"
|
||||||
|
|
||||||
|
if ! resolve_ok "$host"; then
|
||||||
|
echo "[!] DNS zeigt (noch) nicht hierher – überspringe: ${host}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! probe_http "$host"; then
|
||||||
|
echo "[!] ACME-HTTP-Check für ${host} fehlgeschlagen (Port 80/IPv6/Firewall/Nginx prüfen)."
|
||||||
|
# wir versuchen trotzdem – Certbot meldet sich, falls es scheitert
|
||||||
|
fi
|
||||||
|
|
||||||
|
EXTRA_ARGS=()
|
||||||
|
# Für MX den Key wiederverwenden → stabiler TLSA (3 1 1)
|
||||||
|
[[ "$host" == "${MAIL_HOSTNAME}" ]] && EXTRA_ARGS+=(--reuse-key)
|
||||||
|
|
||||||
|
# WICHTIG: Deploy-Wrapper anhängen, damit Symlinks/Nginx gesetzt werden
|
||||||
|
certbot certonly \
|
||||||
|
--agree-tos -m "${LE_MAIL}" --non-interactive \
|
||||||
|
--webroot -w "${ACME_WEBROOT}" -d "${host}" \
|
||||||
|
--deploy-hook /usr/local/sbin/mailwolt-deploy.sh \
|
||||||
|
"${EXTRA_ARGS[@]}" "${CERTBOT_EXTRA[@]}" || true
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "${BASE_DOMAIN}" != "example.com" ]]; then
|
||||||
|
issue "${UI_HOST:-}"
|
||||||
|
issue "${WEBMAIL_HOST:-}"
|
||||||
|
issue "${MAIL_HOSTNAME:-}"
|
||||||
|
|
||||||
|
# Nginx nur neu laden, wenn aktiv
|
||||||
|
if systemctl is-active --quiet nginx; then
|
||||||
|
systemctl reload nginx || true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "[i] BASE_DOMAIN=example.com – LE wird übersprungen."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# FIX: Validierung & Reparatur des Mail-Zertifikats
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
MAIL_SSL_DIR="/etc/ssl/mail"
|
||||||
|
install -d -m 0755 "$MAIL_SSL_DIR"
|
||||||
|
|
||||||
|
MAIL_CERT="${MAIL_SSL_DIR}/fullchain.pem"
|
||||||
|
MAIL_KEY="${MAIL_SSL_DIR}/privkey.pem"
|
||||||
|
|
||||||
|
HOST="${MAIL_HOSTNAME:-}"
|
||||||
|
LE_DIR=""
|
||||||
|
[[ -n "$HOST" ]] && LE_DIR="/etc/letsencrypt/live/${HOST}"
|
||||||
|
|
||||||
|
need_fix=0
|
||||||
|
|
||||||
|
# Ist der vorhandene Key gültig? (leer/nicht vorhanden/ungültig -> fix)
|
||||||
|
if [[ ! -s "$MAIL_KEY" ]] || ! openssl pkey -in "$MAIL_KEY" -noout >/dev/null 2>&1; then
|
||||||
|
need_fix=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Wenn Fix nötig: aus Let's Encrypt Live kopieren
|
||||||
|
if [[ $need_fix -eq 1 ]]; then
|
||||||
|
echo "[!] Ungültiger oder fehlender Mail-Private-Key – versuche Reparatur …"
|
||||||
|
if [[ -n "$LE_DIR" && -r "${LE_DIR}/privkey.pem" && -r "${LE_DIR}/fullchain.pem" ]]; then
|
||||||
|
cp -f "${LE_DIR}/privkey.pem" "$MAIL_KEY"
|
||||||
|
cp -f "${LE_DIR}/fullchain.pem" "$MAIL_CERT"
|
||||||
|
chown root:root "$MAIL_CERT" "$MAIL_KEY"
|
||||||
|
chmod 600 "$MAIL_KEY"
|
||||||
|
chmod 644 "$MAIL_CERT"
|
||||||
|
echo "[+] Zertifikate neu kopiert aus ${LE_DIR}."
|
||||||
|
# Reload NICHT sofort – flaggen für 90-services
|
||||||
|
touch /run/mailwolt.need-dovecot-reload
|
||||||
|
else
|
||||||
|
echo "[!] Konnte ${LE_DIR}/privkey.pem oder fullchain.pem nicht lesen – bitte prüfen."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "[✓] Mail-Zertifikat & -Key sind gültig."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Optionaler Live-Check (nur wenn Host gesetzt)
|
||||||
|
if [[ -n "$HOST" ]]; then
|
||||||
|
if openssl s_client -connect "${HOST}:993" -servername "${HOST}" </dev/null 2>/dev/null \
|
||||||
|
| grep -q "Verify return code: 0"; then
|
||||||
|
echo "[✓] TLS-Handshake erfolgreich auf imaps://${HOST}:993."
|
||||||
|
else
|
||||||
|
echo "[!] TLS-Handshake auf imaps://${HOST}:993 fehlgeschlagen (Dovecot Reload folgt in 90-services, falls Flag gesetzt)."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
@ -0,0 +1,343 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
# --- Helper: sicherer Frontend-Build als APP_USER ---------------------------
|
||||||
|
safe_frontend_build() {
|
||||||
|
echo "[i] Frontend build …"
|
||||||
|
|
||||||
|
# Verzeichnisse & Rechte vorbereiten (Gruppen-sticky & ACL)
|
||||||
|
install -d -m 2775 -o "$APP_USER" -g "$APP_GROUP" \
|
||||||
|
"${APP_DIR}/public/build" "${APP_DIR}/node_modules" "${APP_DIR}/.npm-cache"
|
||||||
|
|
||||||
|
chown -R "$APP_USER":"$APP_GROUP" "${APP_DIR}"
|
||||||
|
find "${APP_DIR}" -type d -exec chmod 2775 {} \;
|
||||||
|
find "${APP_DIR}" -type f -exec chmod 664 {} \;
|
||||||
|
setfacl -R -m g:"$APP_GROUP":rwX -m d:g:"$APP_GROUP":rwX "${APP_DIR}" || true
|
||||||
|
|
||||||
|
# Vite-/Build-Reste bereinigen (falls mal root dort gebaut hat)
|
||||||
|
rm -rf "${APP_DIR}/node_modules/.vite" "${APP_DIR}/public/build/"* 2>/dev/null || true
|
||||||
|
|
||||||
|
# npm auf projektlokales Cache konfigurieren
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "cat > ~/.npmrc <<'RC'
|
||||||
|
fund=false
|
||||||
|
audit=false
|
||||||
|
prefer-offline=true
|
||||||
|
cache=${APP_DIR}/.npm-cache
|
||||||
|
RC"
|
||||||
|
|
||||||
|
# Node ggf. installieren
|
||||||
|
if ! command -v node >/dev/null 2>&1; then
|
||||||
|
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
||||||
|
apt-get install -y nodejs
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Dependencies + Build (als App-User)
|
||||||
|
if sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && (npm ci --no-audit --no-fund || npm install --no-audit --no-fund) && npm run build"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[!] Build fehlgeschlagen – Rechtefix + Clean + Retry …"
|
||||||
|
rm -rf "${APP_DIR}/node_modules/.vite" "${APP_DIR}/public/build/"* 2>/dev/null || true
|
||||||
|
chown -R "$APP_USER":"$APP_GROUP" "${APP_DIR}"
|
||||||
|
find "${APP_DIR}" -type d -exec chmod 2775 {} \;
|
||||||
|
find "${APP_DIR}" -type f -exec chmod 664 {} \;
|
||||||
|
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm run build"
|
||||||
|
}
|
||||||
|
|
||||||
|
relink_and_reload() {
|
||||||
|
if [[ -d /etc/letsencrypt/renewal-hooks/deploy ]]; then
|
||||||
|
run-parts /etc/letsencrypt/renewal-hooks/deploy || true
|
||||||
|
fi
|
||||||
|
if systemctl is-active --quiet nginx; then
|
||||||
|
systemctl reload nginx || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
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://git.nexlab.at/boban/mailwolt.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/Hosts ----------------------------------------------------------
|
||||||
|
#SERVER_PUBLIC_IPV4="${SERVER_PUBLIC_IPV4:-}"
|
||||||
|
#if [[ -z "$SERVER_PUBLIC_IPV4" ]] && command -v curl >/dev/null 2>&1; then
|
||||||
|
# SERVER_PUBLIC_IPV4="$(curl -fsS --max-time 2 https://ifconfig.me 2>/dev/null || true)"
|
||||||
|
# [[ "$SERVER_PUBLIC_IPV4" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || SERVER_PUBLIC_IPV4=""
|
||||||
|
#fi
|
||||||
|
#[[ -n "$SERVER_PUBLIC_IPV4" ]] || SERVER_PUBLIC_IPV4="$(detect_ip)"
|
||||||
|
#
|
||||||
|
#UI_CERT="/etc/ssl/ui/fullchain.pem"
|
||||||
|
#UI_KEY="/etc/ssl/ui/privkey.pem"
|
||||||
|
#
|
||||||
|
#if [[ -n "${UI_HOST:-}" ]]; then
|
||||||
|
# APP_HOST_VAL="$UI_HOST"
|
||||||
|
# APP_URL_VAL="https://${UI_HOST}"
|
||||||
|
#else
|
||||||
|
# APP_HOST_VAL="$SERVER_PUBLIC_IPV4"
|
||||||
|
# SCHEME="http"
|
||||||
|
# [[ -s "$UI_CERT" && -s "$UI_KEY" ]] && SCHEME="https"
|
||||||
|
# APP_URL_VAL="${SCHEME}://${SERVER_PUBLIC_IPV4}"
|
||||||
|
#fi
|
||||||
|
|
||||||
|
SERVER_PUBLIC_IPV4="${SERVER_PUBLIC_IPV4:-}"
|
||||||
|
if [[ -z "$SERVER_PUBLIC_IPV4" ]] && command -v curl >/dev/null 2>&1; then
|
||||||
|
SERVER_PUBLIC_IPV4="$(curl -fsS --max-time 2 https://ifconfig.me 2>/dev/null || true)"
|
||||||
|
[[ "$SERVER_PUBLIC_IPV4" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || SERVER_PUBLIC_IPV4=""
|
||||||
|
fi
|
||||||
|
[[ -n "$SERVER_PUBLIC_IPV4" ]] || SERVER_PUBLIC_IPV4="$(detect_ip)"
|
||||||
|
|
||||||
|
UI_CERT="/etc/ssl/ui/fullchain.pem"
|
||||||
|
UI_KEY="/etc/ssl/ui/privkey.pem"
|
||||||
|
|
||||||
|
# DEV-Modus: immer IP als Host, http (kein example.com / keine Fake-Domain)
|
||||||
|
if [[ "${DEV_MODE:-0}" = "1" || "${APP_ENV:-production}" = "local" ]]; then
|
||||||
|
APP_HOST_VAL="$SERVER_PUBLIC_IPV4"
|
||||||
|
APP_URL_VAL="http://${APP_HOST_VAL}"
|
||||||
|
else
|
||||||
|
# PROD/normal: wenn UI_HOST gesetzt → benutzen, sonst IP
|
||||||
|
if [[ -n "${UI_HOST:-}" ]]; then
|
||||||
|
APP_HOST_VAL="$UI_HOST"
|
||||||
|
APP_URL_VAL="https://${UI_HOST}"
|
||||||
|
else
|
||||||
|
APP_HOST_VAL="$SERVER_PUBLIC_IPV4"
|
||||||
|
SCHEME="http"
|
||||||
|
[[ -s "$UI_CERT" && -s "$UI_KEY" ]] && SCHEME="https"
|
||||||
|
APP_URL_VAL="${SCHEME}://${APP_HOST_VAL}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
SECURE=$([[ "${APP_ENV}" = "production" ]] && echo true || echo false)
|
||||||
|
|
||||||
|
# --- .env schreiben ---------------------------------------------------------
|
||||||
|
upsert_env APP_URL "${APP_URL_VAL}"
|
||||||
|
|
||||||
|
if [[ "${PROXY_MODE:-0}" -eq 1 ]]; then
|
||||||
|
TP_LIST="127.0.0.1,::1"
|
||||||
|
[[ -n "${NPM_IP:-}" ]] && TP_LIST="${TP_LIST},${NPM_IP}"
|
||||||
|
upsert_env TRUSTED_PROXIES "$TP_LIST"
|
||||||
|
upsert_env TRUSTED_HEADERS "x-forwarded-all"
|
||||||
|
else
|
||||||
|
upsert_env TRUSTED_PROXIES ""
|
||||||
|
upsert_env TRUSTED_HEADERS "x-forwarded-all"
|
||||||
|
fi
|
||||||
|
|
||||||
|
upsert_env APP_HOST "${APP_HOST_VAL}"
|
||||||
|
upsert_env APP_NAME "${APP_NAME}"
|
||||||
|
upsert_env APP_ENV "${APP_ENV:-production}"
|
||||||
|
upsert_env APP_DEBUG "${APP_DEBUG:-false}"
|
||||||
|
upsert_env APP_TIMEZONE "${APP_TZ:-UTC}"
|
||||||
|
|
||||||
|
upsert_env APP_LOCALE "${APP_LOCALE:-de}"
|
||||||
|
upsert_env APP_FALLBACK_LOCALE "en"
|
||||||
|
|
||||||
|
upsert_env SERVER_PUBLIC_IPV4 "${SERVER_PUBLIC_IPV4}"
|
||||||
|
upsert_env SERVER_PUBLIC_IPV6 "${SERVER_PUBLIC_IPV6:-}"
|
||||||
|
|
||||||
|
upsert_env SYSMAIL_SUB "${SYSMAIL_SUB}"
|
||||||
|
upsert_env SYSMAIL_DOMAIN "${SYSMAIL_DOMAIN}"
|
||||||
|
upsert_env DKIM_ENABLE "${DKIM_ENABLE}"
|
||||||
|
upsert_env DKIM_SELECTOR "${DKIM_SELECTOR}"
|
||||||
|
upsert_env DKIM_GENERATE "${DKIM_GENERATE}"
|
||||||
|
|
||||||
|
upsert_env BASE_DOMAIN "${BASE_DOMAIN}"
|
||||||
|
upsert_env UI_SUB "${UI_SUB}"
|
||||||
|
upsert_env WEBMAIL_SUB "${WEBMAIL_SUB}"
|
||||||
|
upsert_env MTA_SUB "${MTA_SUB}"
|
||||||
|
upsert_env LE_EMAIL "${LE_EMAIL:-admin@${BASE_DOMAIN}}"
|
||||||
|
|
||||||
|
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_SETTINGS_STORE "redis"
|
||||||
|
upsert_env CACHE_STORE "redis"
|
||||||
|
upsert_env CACHE_DRIVER "redis"
|
||||||
|
upsert_env CACHE_PREFIX "${APP_USER_PREFIX}_cache:"
|
||||||
|
upsert_env SESSION_DRIVER "redis"
|
||||||
|
upsert_env SESSION_SECURE_COOKIE "false" # wird nach SSL-Setup auf true gesetzt
|
||||||
|
upsert_env SESSION_SAMESITE "lax"
|
||||||
|
upsert_env REDIS_CLIENT "phpredis"
|
||||||
|
upsert_env REDIS_HOST "127.0.0.1"
|
||||||
|
upsert_env REDIS_PORT "6379"
|
||||||
|
upsert_env REDIS_PASSWORD "${REDIS_PASS}"
|
||||||
|
upsert_env REDIS_DB "0"
|
||||||
|
upsert_env REDIS_CACHE_DB "1"
|
||||||
|
upsert_env REDIS_CACHE_CONNECTION "cache"
|
||||||
|
upsert_env REDIS_CACHE_LOCK_CONNECTION "default"
|
||||||
|
|
||||||
|
upsert_env BROADCAST_DRIVER "reverb"
|
||||||
|
upsert_env QUEUE_CONNECTION "redis"
|
||||||
|
upsert_env LOG_CHANNEL "daily"
|
||||||
|
|
||||||
|
upsert_env REVERB_APP_ID "${APP_USER_PREFIX}"
|
||||||
|
grep -q '^REVERB_APP_KEY=' "$ENV_FILE" || upsert_env REVERB_APP_KEY "${APP_USER_PREFIX}_$(openssl rand -hex 16)"
|
||||||
|
grep -q '^REVERB_APP_SECRET=' "$ENV_FILE" || upsert_env REVERB_APP_SECRET "${APP_USER_PREFIX}_$(openssl rand -hex 32)"
|
||||||
|
upsert_env REVERB_HOST "\${APP_HOST}"
|
||||||
|
upsert_env REVERB_PORT "443"
|
||||||
|
upsert_env REVERB_SCHEME "https"
|
||||||
|
upsert_env REVERB_PATH "/ws"
|
||||||
|
upsert_env REVERB_SCALING_ENABLED "true"
|
||||||
|
upsert_env REVERB_SCALING_CHANNEL "reverb"
|
||||||
|
|
||||||
|
upsert_env VITE_REVERB_APP_KEY "\${REVERB_APP_KEY}"
|
||||||
|
upsert_env VITE_REVERB_HOST "\${REVERB_HOST}"
|
||||||
|
upsert_env VITE_REVERB_PORT "\${REVERB_PORT}"
|
||||||
|
upsert_env VITE_REVERB_SCHEME "\${REVERB_SCHEME}"
|
||||||
|
upsert_env VITE_REVERB_PATH "\${REVERB_PATH}"
|
||||||
|
|
||||||
|
upsert_env REVERB_SERVER_APP_KEY "\${REVERB_APP_KEY}"
|
||||||
|
upsert_env REVERB_SERVER_HOST "127.0.0.1"
|
||||||
|
upsert_env REVERB_SERVER_PORT "8080"
|
||||||
|
upsert_env REVERB_SERVER_PATH ""
|
||||||
|
upsert_env REVERB_SERVER_SCHEME "http"
|
||||||
|
|
||||||
|
# --- DEV Block (optional) ---------------------------------------------------
|
||||||
|
DEV_MODE="${DEV_MODE:-0}"
|
||||||
|
if [[ "$DEV_MODE" = "1" ]]; then
|
||||||
|
sed -i '/^# --- MailWolt DEV/,/^# --- \/MailWolt DEV/d' "${ENV_FILE}"
|
||||||
|
cat >> "${ENV_FILE}" <<CONF
|
||||||
|
# --- MailWolt DEV ---
|
||||||
|
VITE_DEV_HOST=127.0.0.1
|
||||||
|
VITE_DEV_PORT=5173
|
||||||
|
VITE_HMR_PROTOCOL=wss
|
||||||
|
VITE_HMR_CLIENT_PORT=443
|
||||||
|
VITE_HMR_HOST=${SERVER_PUBLIC_IPV4}
|
||||||
|
VITE_DEV_ORIGIN=$(grep '^APP_URL=' "${ENV_FILE}" | cut -d= -f2-)
|
||||||
|
# --- /MailWolt DEV ---
|
||||||
|
CONF
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Zertifikate in /etc/ssl/* bereitstellen, bevor Laravel irgendwas liest --
|
||||||
|
relink_and_reload
|
||||||
|
|
||||||
|
# --- RECHTE FIXEN: storage & bootstrap/cache (www-data + mailwolt) ----------
|
||||||
|
log "Setze korrekte Rechte für Laravel-Verzeichnisse …"
|
||||||
|
cd "${APP_DIR}"
|
||||||
|
chgrp -R www-data storage bootstrap/cache || true
|
||||||
|
find storage bootstrap/cache -type d -exec chmod 2775 {} \; || true
|
||||||
|
find storage bootstrap/cache -type f -exec chmod 0664 {} \; || true
|
||||||
|
setfacl -R -m u:www-data:rwx,u:${APP_USER}:rwx storage bootstrap/cache || true
|
||||||
|
setfacl -dR -m u:www-data:rwx,u:${APP_USER}:rwx storage bootstrap/cache || true
|
||||||
|
log "[✓] Schreibrechte für Laravel korrigiert."
|
||||||
|
|
||||||
|
# --- DKIM: Verzeichnisse & Basisrechte --------------------------------------
|
||||||
|
install -d -m 2775 -o "$APP_USER" -g www-data "$APP_DIR/storage/app/private"
|
||||||
|
install -d -m 2775 -o "$APP_USER" -g www-data "$APP_DIR/storage/app/private/dkim"
|
||||||
|
setfacl -R -m u:${APP_USER}:rwx,u:www-data:rwx "$APP_DIR/storage/app/private" || true
|
||||||
|
setfacl -dR -m u:${APP_USER}:rwx,u:www-data:rwx "$APP_DIR/storage/app/private" || true
|
||||||
|
|
||||||
|
# --- OpenDKIM: keys & DNS-Verzeichnis --------------------------------------
|
||||||
|
install -d -m 0750 -o opendkim -g opendkim /etc/opendkim
|
||||||
|
install -d -m 0750 -o opendkim -g opendkim /etc/opendkim/keys
|
||||||
|
install -d -m 0755 -o root -g root /etc/mailwolt
|
||||||
|
install -d -m 0755 -o root -g root /etc/mailwolt/dns
|
||||||
|
|
||||||
|
# --- Caches leeren, Migrationen ausführen -----------------------------------
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan optimize:clear"
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan migrate --force"
|
||||||
|
|
||||||
|
# --- Seeder (legt Domains/DKIM etc. an) -------------------------------------
|
||||||
|
if [[ "${BASE_DOMAIN}" != "example.com" ]]; then
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan db:seed --class=SystemDomainSeeder --force"
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan db:seed --class=SystemBackupSeeder --force"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- DKIM für SYSMAIL_DOMAIN via App erzeugen & per Helper einhängen --------
|
||||||
|
DKIM_ENABLE="${DKIM_ENABLE:-1}"
|
||||||
|
DKIM_SELECTOR="${DKIM_SELECTOR:-mwl1}"
|
||||||
|
SYSMAIL_DOMAIN="${SYSMAIL_DOMAIN:-sysmail.${BASE_DOMAIN}}"
|
||||||
|
|
||||||
|
if [[ "${DKIM_ENABLE}" = "1" && -n "${SYSMAIL_DOMAIN}" ]]; then
|
||||||
|
log "Erzeuge/aktualisiere DKIM für ${SYSMAIL_DOMAIN} (Selector: ${DKIM_SELECTOR}) …"
|
||||||
|
|
||||||
|
# 1) In der App generieren (als mailwolt), und Pfad + TXT zurückgeben
|
||||||
|
OUT="$(sudo -u "${APP_USER}" -H bash -lc "
|
||||||
|
set -e
|
||||||
|
cd '${APP_DIR}'
|
||||||
|
php -r '
|
||||||
|
require \"vendor/autoload.php\";
|
||||||
|
\$app=require \"bootstrap/app.php\";
|
||||||
|
\$app->make(Illuminate\\Contracts\\Console\\Kernel::class)->bootstrap();
|
||||||
|
\$d = App\\Models\\Domain::firstOrCreate([\"domain\"=>\"${SYSMAIL_DOMAIN}\"],[\"is_active\"=>1,\"is_system\"=>1]);
|
||||||
|
\$r = app(App\\Services\\DkimService::class)->generateForDomain(\$d, 2048, \"${DKIM_SELECTOR}\");
|
||||||
|
echo \$r[\"priv_path\"], \"\\n\";
|
||||||
|
echo \$r[\"dns_txt\"], \"\\n\";
|
||||||
|
'
|
||||||
|
")"
|
||||||
|
|
||||||
|
PRIV_PATH="$(printf '%s\n' "$OUT" | sed -n '1p')"
|
||||||
|
DNS_TXT="$(printf '%s\n' "$OUT" | sed -n '2,$p')"
|
||||||
|
|
||||||
|
if [[ -z "$PRIV_PATH" || ! -s "$PRIV_PATH" ]]; then
|
||||||
|
echo "[!] DKIM priv_path fehlt oder Datei leer: $PRIV_PATH" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TMP_TXT="$(mktemp /tmp/dkim_txt_XXXXXX.txt)"
|
||||||
|
printf '%s' "$DNS_TXT" >"$TMP_TXT"
|
||||||
|
|
||||||
|
# 2) Root-Helper ausführen (hängt Key ein, pflegt Key/SigningTable, kopiert TXT)
|
||||||
|
if [[ -x /usr/local/sbin/mailwolt-install-dkim ]]; then
|
||||||
|
/usr/local/sbin/mailwolt-install-dkim "${SYSMAIL_DOMAIN}" "${DKIM_SELECTOR}" "${PRIV_PATH}" "${TMP_TXT}"
|
||||||
|
else
|
||||||
|
echo "[!] Helper /usr/local/sbin/mailwolt-install-dkim fehlt oder ist nicht ausführbar." >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$TMP_TXT" || true
|
||||||
|
|
||||||
|
# 3) OpenDKIM neu laden
|
||||||
|
touch /run/mailwolt.need-opendkim-reload || true
|
||||||
|
else
|
||||||
|
log "DKIM übersprungen (DKIM_ENABLE=${DKIM_ENABLE}, SYSMAIL_DOMAIN='${SYSMAIL_DOMAIN}')."
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# --- TLSA aus App heraus (idempotent; läuft, wenn Zert lesbar ist) ----------
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan dns:tlsa:refresh || true"
|
||||||
|
|
||||||
|
# --- Build Frontend (nur wenn nötig) ----------------------------------------
|
||||||
|
if [[ -f "${APP_DIR}/package.json" && ! -f "${APP_DIR}/public/build/manifest.json" ]]; then
|
||||||
|
safe_frontend_build
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Abschluss: Caches + Rechte + Reloads -----------------------------------
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan optimize:clear && php artisan config:cache && php artisan optimize:clear"
|
||||||
|
|
||||||
|
# Konsistente Rechte/ACL für das gesamte App-Verzeichnis
|
||||||
|
chown -R "$APP_USER":"$APP_GROUP" "$APP_DIR"
|
||||||
|
find "$APP_DIR" -type d -exec chmod 2775 {} \;
|
||||||
|
find "$APP_DIR" -type f -exec chmod 664 {} \;
|
||||||
|
setfacl -R -m g:"$APP_GROUP":rwX -m d:g:"$APP_GROUP":rwX "$APP_DIR" || true
|
||||||
|
|
||||||
|
# Laravel-Write-Dirs sicherstellen (mit setgid & ACL)
|
||||||
|
install -d -m 2775 -o "$APP_USER" -g "$APP_GROUP" "$APP_DIR/storage" "$APP_DIR/bootstrap/cache"
|
||||||
|
chgrp -R www-data "$APP_DIR/storage" "$APP_DIR/bootstrap/cache" || true
|
||||||
|
find "$APP_DIR/storage" "$APP_DIR/bootstrap/cache" -type d -exec chmod 2775 {} \; || true
|
||||||
|
find "$APP_DIR/storage" "$APP_DIR/bootstrap/cache" -type f -exec chmod 0664 {} \; || true
|
||||||
|
setfacl -R -m u:www-data:rwx,u:${APP_USER}:rwx "$APP_DIR/storage" "$APP_DIR/bootstrap/cache" || true
|
||||||
|
setfacl -dR -m u:www-data:rwx,u:${APP_USER}:rwx "$APP_DIR/storage" "$APP_DIR/bootstrap/cache" || true
|
||||||
|
|
@ -0,0 +1,264 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
log "Update-Wrapper & Sudoers …"
|
||||||
|
|
||||||
|
WRAPPER="/usr/local/sbin/mailwolt-update"
|
||||||
|
LOGFILE="/var/log/mailwolt-update.log"
|
||||||
|
STATEDIR="/var/lib/mailwolt/update"
|
||||||
|
SUDOERS="/etc/sudoers.d/mailwolt-update"
|
||||||
|
VERSION_FILE="/var/lib/mailwolt/version"
|
||||||
|
SUDOERS_SERVICES="/etc/sudoers.d/mailwolt-services"
|
||||||
|
SUDOERS_ARTISAN="/etc/sudoers.d/mailwolt-artisan"
|
||||||
|
|
||||||
|
# Kandidaten: wo liegt update.sh?
|
||||||
|
CANDIDATES=(
|
||||||
|
/opt/mailwolt-installer/scripts/update.sh
|
||||||
|
/mailwolt-installer/scripts/update.sh
|
||||||
|
/usr/local/lib/mailwolt/update.sh
|
||||||
|
)
|
||||||
|
|
||||||
|
# State/Log vorbereiten
|
||||||
|
install -d -m 0755 "$(dirname "$LOGFILE")"
|
||||||
|
install -d -m 0755 "$STATEDIR"
|
||||||
|
: > "$LOGFILE" || true
|
||||||
|
chmod 0644 "$LOGFILE"
|
||||||
|
|
||||||
|
# Wrapper erzeugen
|
||||||
|
cat > "$WRAPPER" <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
LOG="/var/log/mailwolt-update.log"
|
||||||
|
STATE_DIR="/var/lib/mailwolt/update"
|
||||||
|
APP_DIR="/var/www/mailwolt"
|
||||||
|
WEB_USER="www-data"
|
||||||
|
|
||||||
|
CANDIDATES=(
|
||||||
|
/opt/mailwolt-installer/scripts/update.sh
|
||||||
|
/mailwolt-installer/scripts/update.sh
|
||||||
|
/usr/local/lib/mailwolt/update.sh
|
||||||
|
)
|
||||||
|
|
||||||
|
install -d -m 0755 "$(dirname "$LOG")" "$STATE_DIR" /var/lib/mailwolt
|
||||||
|
: > "$LOG" || true
|
||||||
|
chmod 0644 "$LOG"
|
||||||
|
|
||||||
|
echo "running" > "$STATE_DIR/state"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "===== $(date -Is) :: Update gestartet ====="
|
||||||
|
|
||||||
|
# --- Update-Script finden --------------------------------------------------
|
||||||
|
SCRIPT=""
|
||||||
|
for p in "${CANDIDATES[@]}"; do
|
||||||
|
if [[ -x "$p" ]]; then SCRIPT="$p"; break; fi
|
||||||
|
if [[ -f "$p" && -r "$p" ]]; then SCRIPT="$p"; break; fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$SCRIPT" ]]; then
|
||||||
|
echo "[!] update.sh nicht gefunden (versucht: ${CANDIDATES[*]})"
|
||||||
|
rc=127
|
||||||
|
else
|
||||||
|
echo "[i] benutze: $SCRIPT"
|
||||||
|
if [[ "$(id -u)" -ne 0 ]]; then
|
||||||
|
echo "[!] Bitte als root ausführen"
|
||||||
|
rc=1
|
||||||
|
else
|
||||||
|
if [[ -x "$SCRIPT" ]]; then
|
||||||
|
ALLOW_DIRTY=1 "$SCRIPT"
|
||||||
|
else
|
||||||
|
ALLOW_DIRTY=1 bash "$SCRIPT"
|
||||||
|
fi
|
||||||
|
rc=$?
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "===== $(date -Is) :: Update-Script beendet (rc=$rc) ====="
|
||||||
|
|
||||||
|
# --- Nach dem Update: Assets neu bauen & Laravel optimieren ---------------
|
||||||
|
if [ -d "$APP_DIR" ]; then
|
||||||
|
cd "$APP_DIR" || exit 1
|
||||||
|
|
||||||
|
echo "[i] Führe Composer aus (falls vorhanden) ..."
|
||||||
|
if [ -f composer.json ]; then
|
||||||
|
sudo -u "$WEB_USER" composer install --no-dev --prefer-dist --no-interaction -q || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[i] Baue Frontend-Assets neu ..."
|
||||||
|
if command -v npm >/dev/null 2>&1 && [ -f package.json ]; then
|
||||||
|
sudo -u "$WEB_USER" npm ci --silent || true
|
||||||
|
sudo -u "$WEB_USER" npm run build --silent || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[i] Führe Migrationen & Cache-Optimierungen durch ..."
|
||||||
|
sudo -u "$WEB_USER" php artisan migrate --force || true
|
||||||
|
sudo -u "$WEB_USER" php artisan config:cache || true
|
||||||
|
sudo -u "$WEB_USER" php artisan optimize:clear || true
|
||||||
|
sudo -u "$WEB_USER" php artisan route:cache || true
|
||||||
|
sudo -u "$WEB_USER" php artisan view:cache || true
|
||||||
|
|
||||||
|
echo "[i] Hebe Wartungsmodus auf ..."
|
||||||
|
sudo -u "$WEB_USER" php artisan up >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Version aktualisieren -------------------------------------------------
|
||||||
|
echo "[i] Aktualisiere Version ..."
|
||||||
|
if command -v git >/dev/null 2>&1; then
|
||||||
|
SRC="/var/www/mailwolt"
|
||||||
|
if [ ! -d "$SRC/.git" ]; then
|
||||||
|
SRC="/opt/mailwolt-installer"
|
||||||
|
fi
|
||||||
|
|
||||||
|
git config --global --add safe.directory "$SRC" || true
|
||||||
|
|
||||||
|
if [ -f "$SRC/.git/shallow" ]; then
|
||||||
|
git -C "$SRC" fetch --unshallow --quiet || true
|
||||||
|
fi
|
||||||
|
git -C "$SRC" fetch --tags --quiet origin || true
|
||||||
|
|
||||||
|
raw="$(git -C "$SRC" describe --tags --always --dirty 2>/dev/null || echo "unknown")"
|
||||||
|
norm="$(printf '%s' "$raw" | sed -E 's/^[vV]//; s/-.*$//')"
|
||||||
|
|
||||||
|
printf '%s\n' "$raw" > /var/lib/mailwolt/version_raw
|
||||||
|
printf '%s\n' "$norm" > /var/lib/mailwolt/version
|
||||||
|
chmod 0644 /var/lib/mailwolt/version_raw /var/lib/mailwolt/version
|
||||||
|
|
||||||
|
echo "[i] Version aktualisiert: raw=$raw norm=$norm (Quelle: $SRC)"
|
||||||
|
else
|
||||||
|
echo "unknown" > /var/lib/mailwolt/version_raw
|
||||||
|
echo "0.0.0" > /var/lib/mailwolt/version
|
||||||
|
chmod 0644 /var/lib/mailwolt/version_raw /var/lib/mailwolt/version
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Services neu starten --------------------------------------------------
|
||||||
|
echo "[i] Starte MailWolt-Dienste neu ..."
|
||||||
|
sudo -u "$WEB_USER" php artisan mailwolt:restart-services || true
|
||||||
|
|
||||||
|
# --- Abschluss -------------------------------------------------------------
|
||||||
|
printf '%s\n' "$rc" > "$STATE_DIR/rc"
|
||||||
|
echo "done" > "$STATE_DIR/state"
|
||||||
|
echo "===== $(date -Is) :: Update beendet ====="
|
||||||
|
exit "$rc"
|
||||||
|
|
||||||
|
} | tee -a "$LOG"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod 0755 "$WRAPPER"
|
||||||
|
chown root:root "$WRAPPER"
|
||||||
|
|
||||||
|
# Sudoers: www-data (Laravel) & mailwolt dürfen den Wrapper laufen lassen
|
||||||
|
cat > "$SUDOERS" <<'EOF'
|
||||||
|
Defaults!/usr/local/sbin/mailwolt-update !requiretty
|
||||||
|
www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-update
|
||||||
|
mailwolt ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-update
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chown root:root "$SUDOERS"
|
||||||
|
chmod 440 "$SUDOERS"
|
||||||
|
|
||||||
|
if ! visudo -c -f "$SUDOERS" >/dev/null 2>&1; then
|
||||||
|
echo "[!] Ungültiger sudoers-Eintrag in $SUDOERS – entferne Datei."
|
||||||
|
rm -f "$SUDOERS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > "$SUDOERS_SERVICES" <<'EOF'
|
||||||
|
Defaults!/usr/bin/systemctl !requiretty
|
||||||
|
|
||||||
|
Cmnd_Alias MW_SERVICES = \
|
||||||
|
/usr/bin/systemctl reload nginx.service, \
|
||||||
|
/usr/bin/systemctl try-reload-or-restart nginx.service, \
|
||||||
|
/usr/bin/systemctl try-reload-or-restart postfix.service, \
|
||||||
|
/usr/bin/systemctl try-reload-or-restart dovecot.service, \
|
||||||
|
/usr/bin/systemctl try-reload-or-restart rspamd.service, \
|
||||||
|
/usr/bin/systemctl try-reload-or-restart opendkim.service, \
|
||||||
|
/usr/bin/systemctl try-reload-or-restart opendmarc.service, \
|
||||||
|
/usr/bin/systemctl try-reload-or-restart clamav-daemon.service, \
|
||||||
|
/usr/bin/systemctl try-reload-or-restart redis-server.service
|
||||||
|
|
||||||
|
www-data ALL=(root) NOPASSWD: MW_SERVICES
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod 440 "$SUDOERS_SERVICES"
|
||||||
|
chown root:root "$SUDOERS_SERVICES"
|
||||||
|
|
||||||
|
# Prüfen, ob Syntax gültig ist
|
||||||
|
if ! visudo -c -f "$SUDOERS_SERVICES" >/dev/null 2>&1; then
|
||||||
|
echo "[!] Ungültiger sudoers-Eintrag in $SUDOERS_SERVICES – entferne Datei."
|
||||||
|
rm -f "$SUDOERS_SERVICES"
|
||||||
|
else
|
||||||
|
echo "[✓] Sudoers für Dienststeuerung angelegt: $SUDOERS_SERVICES"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Version-File initial anlegen, falls nicht existiert
|
||||||
|
if [[ ! -f "$VERSION_FILE" ]]; then
|
||||||
|
echo "unknown" > "$VERSION_FILE"
|
||||||
|
chmod 0644 "$VERSION_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > "$SUDOERS_ARTISAN" <<'EOF'
|
||||||
|
# mailwolt darf artisan im App-Verzeichnis als www-data ausführen (ohne Passwort)
|
||||||
|
mailwolt ALL=(www-data) NOPASSWD: /usr/bin/php /var/www/mailwolt/artisan *
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chown root:root "$SUDOERS_ARTISAN"
|
||||||
|
chmod 440 "$SUDOERS_ARTISAN"
|
||||||
|
|
||||||
|
if ! visudo -c -f "$SUDOERS_ARTISAN" >/dev/null 2>&1; then
|
||||||
|
echo "[!] Ungültiger sudoers-Eintrag in $SUDOERS_ARTISAN – entferne Datei."
|
||||||
|
rm -f "$SUDOERS_ARTISAN"
|
||||||
|
else
|
||||||
|
echo "[✓] Sudoers für Artisan-Kommandos angelegt: $SUDOERS_ARTISAN"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "[✓] Update-Wrapper bereit: $WRAPPER"
|
||||||
|
log "[✓] Version wird unter $VERSION_FILE gespeichert"
|
||||||
|
|
||||||
|
# ─── Installer-Wrapper ────────────────────────────────────────────────────────
|
||||||
|
INSTALL_WRAPPER="/usr/local/sbin/mailwolt-install"
|
||||||
|
INSTALL_SUDOERS="/etc/sudoers.d/mailwolt-install"
|
||||||
|
INSTALL_STATE_DIR="/var/lib/mailwolt/install"
|
||||||
|
INSTALL_LOG="/var/log/mailwolt-install.log"
|
||||||
|
|
||||||
|
# State/Log vorbereiten
|
||||||
|
install -d -m 0755 "$INSTALL_STATE_DIR"
|
||||||
|
: > "$INSTALL_LOG" || true
|
||||||
|
chmod 0644 "$INSTALL_LOG"
|
||||||
|
|
||||||
|
# Installer-Wrapper aus scripts/install-wrapper.sh kopieren
|
||||||
|
INSTALL_SRC=""
|
||||||
|
for candidate in \
|
||||||
|
"$(dirname "$0")/install-wrapper.sh" \
|
||||||
|
/opt/mailwolt-installer/scripts/install-wrapper.sh \
|
||||||
|
/var/www/mailwolt/mailwolt-installer/scripts/install-wrapper.sh; do
|
||||||
|
[[ -f "$candidate" ]] && INSTALL_SRC="$candidate" && break
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -n "$INSTALL_SRC" ]]; then
|
||||||
|
cp "$INSTALL_SRC" "$INSTALL_WRAPPER"
|
||||||
|
chmod 0755 "$INSTALL_WRAPPER"
|
||||||
|
chown root:root "$INSTALL_WRAPPER"
|
||||||
|
echo "[✓] Installer-Wrapper angelegt: $INSTALL_WRAPPER"
|
||||||
|
else
|
||||||
|
echo "[!] install-wrapper.sh nicht gefunden – Installer-Wrapper wird übersprungen."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Sudoers: www-data & mailwolt dürfen den Installer-Wrapper laufen lassen
|
||||||
|
cat > "$INSTALL_SUDOERS" <<'SUDOEOF'
|
||||||
|
Defaults!/usr/local/sbin/mailwolt-install !requiretty
|
||||||
|
www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-install
|
||||||
|
www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-install *
|
||||||
|
mailwolt ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-install
|
||||||
|
mailwolt ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-install *
|
||||||
|
SUDOEOF
|
||||||
|
|
||||||
|
chown root:root "$INSTALL_SUDOERS"
|
||||||
|
chmod 440 "$INSTALL_SUDOERS"
|
||||||
|
|
||||||
|
if ! visudo -c -f "$INSTALL_SUDOERS" >/dev/null 2>&1; then
|
||||||
|
echo "[!] Ungültiger sudoers-Eintrag in $INSTALL_SUDOERS – entferne Datei."
|
||||||
|
rm -f "$INSTALL_SUDOERS"
|
||||||
|
else
|
||||||
|
echo "[✓] Sudoers für Installer angelegt: $INSTALL_SUDOERS"
|
||||||
|
fi
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
log "systemd Units (Reverb / Scheduler / Queue / Mail) …"
|
||||||
|
|
||||||
|
cat > /etc/systemd/system/${APP_USER}-ws.service <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=${APP_NAME} WebSocket Backend
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
Environment=NODE_ENV=production WS_PORT=8080
|
||||||
|
User=${APP_USER}
|
||||||
|
Group=${APP_GROUP}
|
||||||
|
WorkingDirectory=${APP_DIR}
|
||||||
|
ExecStartPre=/usr/bin/bash -lc 'test -f .env'
|
||||||
|
ExecStartPre=/usr/bin/bash -lc 'test -d vendor'
|
||||||
|
ExecStart=/usr/bin/php artisan reverb:start --host=127.0.0.1 --port=8080 --no-interaction
|
||||||
|
Restart=always
|
||||||
|
RestartSec=2
|
||||||
|
StandardOutput=append:/var/log/${APP_USER}-ws.log
|
||||||
|
StandardError=append:/var/log/${APP_USER}-ws.log
|
||||||
|
KillSignal=SIGINT
|
||||||
|
TimeoutStopSec=15
|
||||||
|
UMask=0002
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > /etc/systemd/system/${APP_USER}-schedule.service <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=${APP_NAME} Laravel Scheduler
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=${APP_USER}
|
||||||
|
Group=${APP_GROUP}
|
||||||
|
WorkingDirectory=${APP_DIR}
|
||||||
|
ExecStartPre=/usr/bin/bash -lc 'test -f .env'
|
||||||
|
ExecStartPre=/usr/bin/bash -lc 'test -d vendor'
|
||||||
|
ExecStart=/usr/bin/php artisan schedule:work
|
||||||
|
Restart=always
|
||||||
|
RestartSec=2
|
||||||
|
StandardOutput=append:/var/log/${APP_USER}-schedule.log
|
||||||
|
StandardError=append:/var/log/${APP_USER}-schedule.log
|
||||||
|
KillSignal=SIGINT
|
||||||
|
TimeoutStopSec=15
|
||||||
|
UMask=0002
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > /etc/systemd/system/${APP_USER}-queue.service <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=${APP_NAME} Queue Worker
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=${APP_USER}
|
||||||
|
Group=${APP_GROUP}
|
||||||
|
WorkingDirectory=${APP_DIR}
|
||||||
|
ExecStartPre=/usr/bin/bash -lc 'test -f .env'
|
||||||
|
ExecStartPre=/usr/bin/bash -lc 'test -d vendor'
|
||||||
|
ExecStart=/usr/bin/php artisan queue:work --queue=default,notify --tries=1
|
||||||
|
Restart=always
|
||||||
|
RestartSec=2
|
||||||
|
StandardOutput=append:/var/log/${APP_USER}-queue.log
|
||||||
|
StandardError=append:/var/log/${APP_USER}-queue.log
|
||||||
|
KillSignal=SIGINT
|
||||||
|
TimeoutStopSec=15
|
||||||
|
UMask=0002
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chown root:root /etc/systemd/system/${APP_USER}-*.service
|
||||||
|
chmod 644 /etc/systemd/system/${APP_USER}-*.service
|
||||||
|
touch /var/log/${APP_USER}-ws.log /var/log/${APP_USER}-schedule.log /var/log/${APP_USER}-queue.log
|
||||||
|
chown ${APP_USER}:${APP_GROUP} /var/log/${APP_USER}-*.log
|
||||||
|
chmod 664 /var/log/${APP_USER}-*.log
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
|
||||||
|
# App-Dienste
|
||||||
|
if sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan list --no-ansi | grep -qE '(^| )reverb:start( |$)'"; then
|
||||||
|
systemctl enable --now ${APP_USER}-ws
|
||||||
|
else
|
||||||
|
systemctl disable --now ${APP_USER}-ws >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
systemctl enable --now ${APP_USER}-schedule
|
||||||
|
systemctl enable --now ${APP_USER}-queue
|
||||||
|
|
||||||
|
# Mail-Dienste starten
|
||||||
|
systemctl enable --now rspamd opendkim postfix dovecot || true
|
||||||
|
|
||||||
|
# PHP-FPM: Unit erkennen, enable + (re)load
|
||||||
|
enable_and_touch_php_fpm() {
|
||||||
|
for u in php8.3-fpm php8.2-fpm php8.1-fpm php8.0-fpm php7.4-fpm php-fpm; do
|
||||||
|
if systemctl list-unit-files | grep -q "^${u}\.service"; then
|
||||||
|
systemctl enable --now "$u" || true
|
||||||
|
systemctl reload "$u" || systemctl restart "$u" || true
|
||||||
|
echo "[i] PHP-FPM unit: $u"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "[!] Keine passende php-fpm Unit gefunden."
|
||||||
|
}
|
||||||
|
enable_and_touch_php_fpm
|
||||||
|
|
||||||
|
# Falls in 80-app.sh DKIM installiert wurde: jetzt einmal reloaden
|
||||||
|
if [[ -e /run/mailwolt.need-opendkim-reload ]]; then
|
||||||
|
systemctl reload opendkim || true
|
||||||
|
rm -f /run/mailwolt.need-opendkim-reload || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Falls Zert-Fix markiert ist: Dovecot neu laden
|
||||||
|
if [[ -e /run/mailwolt.need-dovecot-reload ]]; then
|
||||||
|
systemctl reload dovecot || true
|
||||||
|
rm -f /run/mailwolt.need-dovecot-reload || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Falls DB-Migration schon durch: einmal reload
|
||||||
|
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
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Mini-Portcheck (hilft beim Installer-Output)
|
||||||
|
echo "Listening (25/465/587):"
|
||||||
|
ss -ltnp | awk '$4 ~ /:(25|465|587)$/ {print " " $0}'
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
log "Sudoers: npm-Build ohne Passwort für user 'mailwolt' …"
|
||||||
|
|
||||||
|
# 1) npm-Binary ermitteln (normal: /usr/bin/npm)
|
||||||
|
NPM_BIN="$(command -v npm || true)"
|
||||||
|
|
||||||
|
if [[ -z "$NPM_BIN" ]]; then
|
||||||
|
warn "npm wurde nicht gefunden – sudoers wird vorbereitet, aber ohne Validierung. Stelle sicher, dass Node/npm installiert ist."
|
||||||
|
# Fallback – die meisten Distros legen hier an
|
||||||
|
NPM_BIN="/usr/bin/npm"
|
||||||
|
fi
|
||||||
|
|
||||||
|
SUDOERS_FILE="/etc/sudoers.d/mailwolt-npm"
|
||||||
|
|
||||||
|
# 2) Sudoers-Datei schreiben
|
||||||
|
cat > "$SUDOERS_FILE" <<EOF
|
||||||
|
Defaults!${NPM_BIN} !requiretty
|
||||||
|
mailwolt ALL=(root) NOPASSWD: ${NPM_BIN}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chown root:root "$SUDOERS_FILE"
|
||||||
|
chmod 440 "$SUDOERS_FILE"
|
||||||
|
|
||||||
|
# 3) Validieren
|
||||||
|
if visudo -c -f "$SUDOERS_FILE" >/dev/null 2>&1; then
|
||||||
|
log "[✓] sudoers OK: ${SUDOERS_FILE} erlaubt 'mailwolt' → ${NPM_BIN} ohne Passwort."
|
||||||
|
else
|
||||||
|
echo "[!] Ungültiger sudoers-Eintrag in ${SUDOERS_FILE} – entferne Datei."
|
||||||
|
rm -f "$SUDOERS_FILE"
|
||||||
|
fi
|
||||||
|
|
@ -0,0 +1,284 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
log "Backup/Restore – Tools, Config & Timer (installer.env) …"
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# 1) installer.env laden (ENV > installer.env > Defaults)
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
if [[ -f /etc/mailwolt/installer.env ]]; then
|
||||||
|
# automatisch exportieren, damit ${VAR} später überall wirkt
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source /etc/mailwolt/installer.env
|
||||||
|
set +a
|
||||||
|
else
|
||||||
|
log "[i] /etc/mailwolt/installer.env nicht gefunden – nutze Defaults."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# 2) Pfade & Defaults (werden durch ENV/installer.env überschrieben)
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
CONF_DIR="/etc/mailwolt"
|
||||||
|
CONF_FILE="${CONF_DIR}/backup.conf"
|
||||||
|
BIN_DIR="/usr/local/sbin"
|
||||||
|
UNIT_DIR="/etc/systemd/system"
|
||||||
|
|
||||||
|
APP_DIR="${APP_DIR:-/var/www/mailwolt}"
|
||||||
|
|
||||||
|
# DB-Parameter aus installer.env (bzw. ENV) oder Fallbacks
|
||||||
|
DB_HOST="${DB_HOST:-127.0.0.1}"
|
||||||
|
DB_NAME="${DB_NAME:-mailwolt}"
|
||||||
|
DB_USER="${DB_USER:-mailwolt}"
|
||||||
|
DB_PASS="${DB_PASS:-}"
|
||||||
|
|
||||||
|
# Backup-Settings aus installer.env (bzw. ENV)
|
||||||
|
BACKUP_DIR="${BACKUP_DIR:-/var/backups/mailwolt}"
|
||||||
|
BACKUP_RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-7}"
|
||||||
|
BACKUP_USE_ZSTD="${BACKUP_USE_ZSTD:-1}"
|
||||||
|
BACKUP_ENABLED="${BACKUP_ENABLED:-0}" # 0|1
|
||||||
|
BACKUP_INTERVAL="${BACKUP_INTERVAL:-daily}" # daily|weekly|monthly
|
||||||
|
|
||||||
|
install -d -m 0755 "$CONF_DIR" "$BACKUP_DIR"
|
||||||
|
|
||||||
|
|
||||||
|
SUDOERS_BACKUP_FILE="/etc/sudoers.d/mailwolt-backup"
|
||||||
|
# 2) Sudoers-Datei schreiben
|
||||||
|
cat > "${SUDOERS_BACKUP_FILE} " <<EOF
|
||||||
|
Defaults!/usr/local/sbin/mailwolt-backup !requiretty
|
||||||
|
www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-backup
|
||||||
|
mailwolt ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-backup
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chown root:root "${SUDOERS_BACKUP_FILE}"
|
||||||
|
chmod 440 "${SUDOERS_BACKUP_FILE}"
|
||||||
|
if ! visudo -c -f "${SUDOERS_BACKUP_FILE}" >/dev/null 2>&1; then
|
||||||
|
echo "[!] Ungültiger sudoers-Eintrag in ${SUDOERS_BACKUP_FILE} – entferne Datei."
|
||||||
|
rm -f "${SUDOERS_BACKUP_FILE}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# 3) /etc/mailwolt/backup.conf (von UI/APP überschreibbar)
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
cat > "$CONF_FILE" <<EOF
|
||||||
|
# MailWolt Backup Konfiguration (UI kann überschreiben)
|
||||||
|
APP_DIR="$APP_DIR"
|
||||||
|
BACKUP_DIR="$BACKUP_DIR"
|
||||||
|
RETENTION_DAYS="$BACKUP_RETENTION_DAYS"
|
||||||
|
USE_ZSTD="$BACKUP_USE_ZSTD"
|
||||||
|
|
||||||
|
# DB-Parameter
|
||||||
|
MYSQL_DB="$DB_NAME"
|
||||||
|
MYSQL_USER="$DB_USER"
|
||||||
|
MYSQL_PASS="$DB_PASS"
|
||||||
|
MYSQL_HOST="$DB_HOST"
|
||||||
|
MYSQL_PORT="3306"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod 0644 "$CONF_FILE"
|
||||||
|
log "[✓] config geschrieben: $CONF_FILE"
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# 4) /usr/local/sbin/mailwolt-backup (schreibt backup.status)
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
cat > "${BIN_DIR}/mailwolt-backup" <<'EOSH'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
log(){ echo "[$(date -Is)] $*"; }
|
||||||
|
|
||||||
|
# Konfiguration laden (ENV > Datei)
|
||||||
|
CONF="/etc/mailwolt/backup.conf"
|
||||||
|
[[ -f "$CONF" ]] && # shellcheck disable=SC1090
|
||||||
|
source "$CONF"
|
||||||
|
|
||||||
|
APP_DIR="${APP_DIR:-/var/www/mailwolt}"
|
||||||
|
BACKUP_DIR="${BACKUP_DIR:-/var/backups/mailwolt}"
|
||||||
|
RETENTION_DAYS="${RETENTION_DAYS:-7}"
|
||||||
|
USE_ZSTD="${USE_ZSTD:-1}"
|
||||||
|
|
||||||
|
MYSQL_DB="${MYSQL_DB:-mailwolt}"
|
||||||
|
MYSQL_USER="${MYSQL_USER:-mailwolt}"
|
||||||
|
MYSQL_PASS="${MYSQL_PASS:-}"
|
||||||
|
MYSQL_HOST="${MYSQL_HOST:-127.0.0.1}"
|
||||||
|
MYSQL_PORT="${MYSQL_PORT:-3306}"
|
||||||
|
|
||||||
|
STATE_DIR="/var/lib/mailwolt"
|
||||||
|
STATUS_FILE="${STATE_DIR}/backup.status"
|
||||||
|
install -d -m 0755 "$STATE_DIR" "$BACKUP_DIR"
|
||||||
|
|
||||||
|
START_TS="$(date +%s)"
|
||||||
|
TS="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||||
|
TMP="$(mktemp -d /tmp/mwbackup.XXXXXX)"
|
||||||
|
trap 'rm -rf "$TMP"' EXIT
|
||||||
|
|
||||||
|
fail(){
|
||||||
|
local msg="${1:-backup failed}"
|
||||||
|
local now="$(date -Is)"
|
||||||
|
{
|
||||||
|
echo "time=${now}"
|
||||||
|
echo "size=0"
|
||||||
|
echo "dur=$(( $(date +%s) - START_TS ))s"
|
||||||
|
echo "ok=0"
|
||||||
|
echo "error=${msg}"
|
||||||
|
} > "$STATUS_FILE"
|
||||||
|
echo "[$now] ${msg}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
trap 'fail "unexpected error (exit $?)"' ERR
|
||||||
|
|
||||||
|
OUT="${BACKUP_DIR}/mailwolt-${TS}.tar"
|
||||||
|
log "⇒ starte Backup in $OUT …"
|
||||||
|
|
||||||
|
# 1) DB
|
||||||
|
log " • mysqldump …"
|
||||||
|
MYSQL_PWD="$MYSQL_PASS" mysqldump \
|
||||||
|
-h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" \
|
||||||
|
--single-transaction --routines --events --triggers \
|
||||||
|
"$MYSQL_DB" > "$TMP/mysql.sql"
|
||||||
|
|
||||||
|
# 2) Maildir
|
||||||
|
log " • Maildir …"
|
||||||
|
tar -C / -cf "$TMP/mail.tar" var/mail/vhosts 2>/dev/null || true
|
||||||
|
|
||||||
|
# 3) App (ohne heavy dirs)
|
||||||
|
log " • App …"
|
||||||
|
tar -C / -cf "$TMP/app.tar" \
|
||||||
|
--exclude='var/www/mailwolt/vendor' \
|
||||||
|
--exclude='var/www/mailwolt/node_modules' \
|
||||||
|
--exclude='var/www/mailwolt/public/build' \
|
||||||
|
var/www/mailwolt
|
||||||
|
|
||||||
|
# 4) Configs
|
||||||
|
log " • Configs …"
|
||||||
|
mkdir -p "$TMP/files"
|
||||||
|
cp -a /etc/mailwolt "$TMP/files/" 2>/dev/null || true
|
||||||
|
cp -a /etc/postfix "$TMP/files/" 2>/dev/null || true
|
||||||
|
cp -a /etc/dovecot "$TMP/files/" 2>/dev/null || true
|
||||||
|
cp -a /etc/opendkim "$TMP/files/" 2>/dev/null || true
|
||||||
|
cp -a /etc/opendmarc "$TMP/files/" 2>/dev/null || true
|
||||||
|
cp -a /etc/rspamd "$TMP/files/" 2>/dev/null || true
|
||||||
|
cp -a /etc/ssl/ui "$TMP/files/" 2>/dev/null || true
|
||||||
|
tar -C "$TMP" -cf "$TMP/files.tar" files
|
||||||
|
|
||||||
|
# 5) Paket
|
||||||
|
log " • Archiviere …"
|
||||||
|
tar -C "$TMP" -cf "$OUT" mysql.sql mail.tar app.tar files.tar
|
||||||
|
|
||||||
|
# 6) Komprimieren (optional)
|
||||||
|
if [[ "${USE_ZSTD:-1}" = "1" ]] && command -v zstd >/dev/null 2>&1; then
|
||||||
|
log " • komprimiere (zstd) …"
|
||||||
|
zstd -f --rm -19 "$OUT"
|
||||||
|
OUT="${OUT}.zst"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 7) Retention
|
||||||
|
if [[ "$RETENTION_DAYS" =~ ^[0-9]+$ ]]; then
|
||||||
|
log " • Retention: lösche älter als ${RETENTION_DAYS} Tage …"
|
||||||
|
find "$BACKUP_DIR" -type f -mtime +"$RETENTION_DAYS" -name 'mailwolt-*' -delete || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 8) Statusfile fürs UI
|
||||||
|
SIZE_BYTES="$(stat -c '%s' "$OUT" 2>/dev/null || echo 0)"
|
||||||
|
{
|
||||||
|
echo "time=$(date -Is)"
|
||||||
|
echo "size=${SIZE_BYTES}"
|
||||||
|
echo "dur=$(( $(date +%s) - START_TS ))s"
|
||||||
|
echo "ok=1"
|
||||||
|
echo "file=${OUT}"
|
||||||
|
} > "$STATUS_FILE"
|
||||||
|
chmod 0644 "$STATUS_FILE" 2>/dev/null || true
|
||||||
|
|
||||||
|
log "[✓] Backup fertig: $OUT"
|
||||||
|
EOSH
|
||||||
|
chmod 0755 "${BIN_DIR}/mailwolt-backup"
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# 5) /usr/local/sbin/mailwolt-restore
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
cat > "${BIN_DIR}/mailwolt-restore" <<'EOSH'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
log(){ echo "[$(date -Is)] $*"; }
|
||||||
|
|
||||||
|
ARCHIVE="${1:-}"
|
||||||
|
[[ -n "$ARCHIVE" ]] || { echo "Usage: mailwolt-restore <backup.tar[.zst]>"; exit 1; }
|
||||||
|
[[ -f "$ARCHIVE" ]] || { echo "Backup nicht gefunden: $ARCHIVE"; exit 1; }
|
||||||
|
|
||||||
|
TMP="$(mktemp -d /tmp/mwrestore.XXXXXX)"
|
||||||
|
trap 'rm -rf "$TMP"' EXIT
|
||||||
|
|
||||||
|
case "$ARCHIVE" in
|
||||||
|
*.zst) zstd -d -c "$ARCHIVE" > "$TMP/backup.tar" ;;
|
||||||
|
*) cp -a "$ARCHIVE" "$TMP/backup.tar" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
log "⇒ entpacke …"
|
||||||
|
tar -C "$TMP" -xf "$TMP/backup.tar"
|
||||||
|
|
||||||
|
# Reihenfolge: DB → App → Mail → Config
|
||||||
|
if [[ -f "$TMP/mysql.sql" ]]; then
|
||||||
|
log " • MySQL wiederherstellen …"
|
||||||
|
mysql < "$TMP/mysql.sql"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$TMP/app.tar" ]]; then
|
||||||
|
log " • App → /var/www/mailwolt …"
|
||||||
|
tar -C / -xf "$TMP/app.tar"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$TMP/mail.tar" ]]; then
|
||||||
|
log " • Maildir → /var/mail/vhosts …"
|
||||||
|
tar -C / -xf "$TMP/mail.tar"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$TMP/files.tar" ]]; then
|
||||||
|
log " • Configs → /etc/* …"
|
||||||
|
tar -C / -xf "$TMP/files.tar"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "[✓] Restore abgeschlossen."
|
||||||
|
EOSH
|
||||||
|
chmod 0755 "${BIN_DIR}/mailwolt-restore"
|
||||||
|
|
||||||
|
log "[✓] Tools installiert: ${BIN_DIR}/mailwolt-backup, mailwolt-restore"
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# 6) systemd Service + Timer (Timer default via installer.env)
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
cat > "${UNIT_DIR}/mailwolt-backup.service" <<'EOSVC'
|
||||||
|
[Unit]
|
||||||
|
Description=MailWolt Backup
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/local/sbin/mailwolt-backup
|
||||||
|
Nice=10
|
||||||
|
IOSchedulingClass=best-effort
|
||||||
|
IOSchedulingPriority=7
|
||||||
|
EOSVC
|
||||||
|
|
||||||
|
cat > "${UNIT_DIR}/mailwolt-backup.timer" <<EOTIM
|
||||||
|
[Unit]
|
||||||
|
Description=MailWolt Backup Timer
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=${BACKUP_ONCALENDAR:-*-*-* 03:00:00}
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
EOTIM
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
|
||||||
|
if [[ "${BACKUP_ENABLED}" = "1" ]]; then
|
||||||
|
log "Aktiviere Backup-Timer (${BACKUP_ONCALENDAR}) …"
|
||||||
|
systemctl enable --now mailwolt-backup.timer
|
||||||
|
else
|
||||||
|
log "Timer bleibt deaktiviert (BACKUP_ENABLED=0)."
|
||||||
|
systemctl disable --now mailwolt-backup.timer >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "[✓] Backup-Setup abgeschlossen."
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
#!/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
|
||||||
|
|
||||||
|
log "[✓] Monit konfiguriert und gestartet"
|
||||||
|
|
||||||
|
# ── mailwolt-update ins System kopieren ─────────────────────────────
|
||||||
|
install -m 0750 -o root -g root scripts/update.sh /usr/local/sbin/mailwolt-update
|
||||||
|
log "[✓] mailwolt-update installiert → ausführbar via 'sudo mailwolt-update'"
|
||||||
|
|
@ -0,0 +1,491 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
log "WoltGuard (Monit + Self-Heal) einrichten …"
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# Env nur nachladen, wenn Flags nicht bereits exportiert sind
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
INSTALLER_ENV="/etc/mailwolt/installer.env"
|
||||||
|
: "${CLAMAV_ENABLE:=}" ; : "${OPENDMARC_ENABLE:=}" ; : "${FAIL2BAN_ENABLE:=}"
|
||||||
|
if [[ -z "${CLAMAV_ENABLE}${OPENDMARC_ENABLE}${FAIL2BAN_ENABLE}" && -r "$INSTALLER_ENV" ]]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "$INSTALLER_ENV"
|
||||||
|
fi
|
||||||
|
CLAMAV_ENABLE="${CLAMAV_ENABLE:-0}"
|
||||||
|
OPENDMARC_ENABLE="${OPENDMARC_ENABLE:-0}"
|
||||||
|
FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}"
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# Monit installieren & aktivieren
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
command -v monit >/dev/null || { apt-get update -qq; apt-get install -y monit; }
|
||||||
|
systemctl enable --now monit
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# Helper-Skripte (laufen später eigenständig → Env selbst laden)
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
install -d -m 0755 /usr/local/sbin
|
||||||
|
|
||||||
|
# Redis-Ping (nimmt REDIS_PASSWORD aus installer.env oder .env)
|
||||||
|
cat >/usr/local/sbin/mailwolt-redis-ping.sh <<'EOSH'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
INSTALLER_ENV="/etc/mailwolt/installer.env"
|
||||||
|
APP_ENV="/var/www/mailwolt/.env"
|
||||||
|
|
||||||
|
# Defaults
|
||||||
|
REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
|
||||||
|
REDIS_PORT="${REDIS_PORT:-6379}"
|
||||||
|
REDIS_PASSWORD="${REDIS_PASSWORD:-}"
|
||||||
|
REDIS_PASS="${REDIS_PASS:-}" # Legacy
|
||||||
|
|
||||||
|
# Installer-Env (falls vorhanden)
|
||||||
|
[[ -r "$INSTALLER_ENV" ]] && . "$INSTALLER_ENV" || true
|
||||||
|
|
||||||
|
# Falls .env existiert: Werte ergänzen, die noch leer sind
|
||||||
|
if [[ -r "$APP_ENV" ]]; then
|
||||||
|
[[ -z "${REDIS_HOST}" ]] && REDIS_HOST="$(grep -m1 -E '^REDIS_HOST=' "$APP_ENV" | cut -d= -f2- || true)"
|
||||||
|
[[ -z "${REDIS_PORT}" ]] && REDIS_PORT="$(grep -m1 -E '^REDIS_PORT=' "$APP_ENV" | cut -d= -f2- || true)"
|
||||||
|
[[ -z "${REDIS_PASSWORD}" ]] && REDIS_PASSWORD="$(grep -m1 -E '^REDIS_PASSWORD=' "$APP_ENV" | cut -d= -f2- || true)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Legacy-Fallback: wenn PASSWORD leer, aber PASS gesetzt → übernehmen
|
||||||
|
[[ -z "${REDIS_PASSWORD}" && -n "${REDIS_PASS}" ]] && REDIS_PASSWORD="$REDIS_PASS"
|
||||||
|
|
||||||
|
# Quotes strippen
|
||||||
|
strip(){ printf '%s' "$1" | sed -E 's/^"(.*)"$/\1/; s/^'\''(.*)'\''$/\1/'; }
|
||||||
|
REDIS_HOST="$(strip "${REDIS_HOST:-}")"
|
||||||
|
REDIS_PORT="$(strip "${REDIS_PORT:-}")"
|
||||||
|
REDIS_PASSWORD="$(strip "${REDIS_PASSWORD:-}")"
|
||||||
|
|
||||||
|
# redis-cli muss vorhanden sein
|
||||||
|
command -v redis-cli >/dev/null 2>&1 || exit 1
|
||||||
|
|
||||||
|
BASE=(timeout 2 redis-cli --no-auth-warning --raw -h "$REDIS_HOST" -p "$REDIS_PORT")
|
||||||
|
if [[ -n "$REDIS_PASSWORD" ]]; then
|
||||||
|
CMD=("${BASE[@]}" -a "$REDIS_PASSWORD" ping)
|
||||||
|
else
|
||||||
|
CMD=("${BASE[@]}" ping)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Erfolgreich nur bei exakt "PONG"
|
||||||
|
[[ "$("${CMD[@]}" 2>/dev/null || true)" == "PONG" ]]
|
||||||
|
EOSH
|
||||||
|
chmod 0755 /usr/local/sbin/mailwolt-redis-ping.sh
|
||||||
|
|
||||||
|
# Rspamd-Heal (setzt Laufzeitverzeichnis, leert alte Socke, restarts rspamd)
|
||||||
|
cat >/usr/local/sbin/mailwolt-rspamd-heal.sh <<'EOSH'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
INSTALLER_ENV="/etc/mailwolt/installer.env"
|
||||||
|
APP_ENV="/var/www/mailwolt/.env"
|
||||||
|
|
||||||
|
REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
|
||||||
|
REDIS_PORT="${REDIS_PORT:-6379}"
|
||||||
|
REDIS_PASSWORD="${REDIS_PASSWORD:-}"
|
||||||
|
|
||||||
|
[[ -r "$INSTALLER_ENV" ]] && . "$INSTALLER_ENV"
|
||||||
|
if [[ -z "${REDIS_PASSWORD}" && -r "$APP_ENV" ]]; then
|
||||||
|
REDIS_PASSWORD="$(grep -E '^REDIS_PASSWORD=' "$APP_ENV" | head -n1 | cut -d= -f2- || true)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Rspamd Runtime fixen
|
||||||
|
install -d -m 0755 -o _rspamd -g _rspamd /run/rspamd || true
|
||||||
|
[[ -S /var/lib/rspamd/rspamd.sock ]] && rm -f /var/lib/rspamd/rspamd.sock || true
|
||||||
|
|
||||||
|
# Neustart
|
||||||
|
systemctl restart rspamd
|
||||||
|
|
||||||
|
# Mini-Healthcheck
|
||||||
|
sleep 2
|
||||||
|
ss -tln | grep -q ':11334' || echo "[WARN] Rspamd Controller Port 11334 nicht sichtbar"
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
EOSH
|
||||||
|
chmod 0755 /usr/local/sbin/mailwolt-rspamd-heal.sh
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# WoltGuard Wrapper + Unit
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
cat >/usr/local/bin/woltguard <<'EOSH'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
case "${1:-status}" in
|
||||||
|
start) systemctl enable --now monit ;;
|
||||||
|
stop) systemctl stop monit ;;
|
||||||
|
status) monit summary || systemctl status monit || true ;;
|
||||||
|
heal) monit reload || true; sleep 1; monit restart all || true ;;
|
||||||
|
monitor) monit monitor all || true ;;
|
||||||
|
unmonitor) monit unmonitor all || true ;;
|
||||||
|
*) echo "Usage: woltguard {start|stop|status|heal|monitor|unmonitor}"; exit 2;;
|
||||||
|
esac
|
||||||
|
EOSH
|
||||||
|
chmod 0755 /usr/local/bin/woltguard
|
||||||
|
|
||||||
|
cat >/etc/systemd/system/woltguard.service <<'EOF'
|
||||||
|
[Unit]
|
||||||
|
Description=WoltGuard – Self-Healing Monitor for MailWolt
|
||||||
|
After=network.target
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/local/bin/woltguard start
|
||||||
|
ExecStop=/usr/local/bin/woltguard stop
|
||||||
|
RemainAfterExit=yes
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now woltguard
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# Monit Basis + includes
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
sed -i 's/^set daemon .*/set daemon 30/' /etc/monit/monitrc || true
|
||||||
|
grep -q 'include /etc/monit/conf.d/*' /etc/monit/monitrc || echo 'include /etc/monit/conf.d/*' >>/etc/monit/monitrc
|
||||||
|
install -d -m 0755 /etc/monit/conf.d
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# Monit Checks
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# 10 – Redis zuerst (abhängig für rspamd)
|
||||||
|
cat >/etc/monit/conf.d/10-redis.conf <<'EOF'
|
||||||
|
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 host 127.0.0.1 port 6379 for 2 cycles then restart
|
||||||
|
if 5 restarts within 5 cycles then alert
|
||||||
|
|
||||||
|
check program redis_ping path "/usr/local/sbin/mailwolt-redis-ping.sh"
|
||||||
|
if status != 0 for 2 cycles then exec "/bin/systemctl restart redis-server"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 20 – Rspamd (hängt von Redis ab), robust über process-matching
|
||||||
|
cat >/etc/monit/conf.d/20-rspamd.conf <<'EOF'
|
||||||
|
check process rspamd matching "/usr/bin/rspamd"
|
||||||
|
start program = "/bin/systemctl start rspamd"
|
||||||
|
stop program = "/bin/systemctl stop rspamd"
|
||||||
|
depends on redis
|
||||||
|
if failed host 127.0.0.1 port 11333 for 2 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh"
|
||||||
|
if failed host 127.0.0.1 port 11334 for 2 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh"
|
||||||
|
if 5 restarts within 5 cycles then alert
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 30 – Maildienste
|
||||||
|
cat >/etc/monit/conf.d/30-postfix.conf <<'EOF'
|
||||||
|
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 tcpssl then restart
|
||||||
|
if failed port 587 type tcp then restart
|
||||||
|
if 5 restarts within 5 cycles then alert
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat >/etc/monit/conf.d/30-dovecot.conf <<'EOF'
|
||||||
|
check process dovecot with pidfile /run/dovecot/master.pid
|
||||||
|
start program = "/bin/systemctl start dovecot"
|
||||||
|
stop program = "/bin/systemctl stop dovecot"
|
||||||
|
if failed port 993 type tcpssl for 2 cycles then restart
|
||||||
|
if failed port 24 protocol lmtp for 2 cycles then restart
|
||||||
|
if 5 restarts within 5 cycles then alert
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 40 – Web/PHP
|
||||||
|
cat >/etc/monit/conf.d/40-nginx.conf <<'EOF'
|
||||||
|
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 tcpssl then restart
|
||||||
|
if 5 restarts within 5 cycles then alert
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 50 – DKIM/DMARC
|
||||||
|
cat >/etc/monit/conf.d/50-opendkim.conf <<'EOF'
|
||||||
|
check process opendkim with pidfile /run/opendkim/opendkim.pid
|
||||||
|
start program = "/bin/systemctl start opendkim"
|
||||||
|
stop program = "/bin/systemctl stop opendkim"
|
||||||
|
if failed host 127.0.0.1 port 8891 type tcp for 2 cycles then restart
|
||||||
|
if 5 restarts within 5 cycles then alert
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# optional: OpenDMARC
|
||||||
|
if [[ "$OPENDMARC_ENABLE" = "1" ]]; then
|
||||||
|
cat >/etc/monit/conf.d/55-opendmarc.conf <<'EOF'
|
||||||
|
check process opendmarc with pidfile /run/opendmarc/opendmarc.pid
|
||||||
|
start program = "/bin/systemctl start opendmarc"
|
||||||
|
stop program = "/bin/systemctl stop opendmarc"
|
||||||
|
if 5 restarts within 5 cycles then alert
|
||||||
|
EOF
|
||||||
|
else
|
||||||
|
rm -f /etc/monit/conf.d/55-opendmarc.conf || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 60 – optional: ClamAV
|
||||||
|
if [[ "$CLAMAV_ENABLE" = "1" ]]; then
|
||||||
|
cat >/etc/monit/conf.d/60-clamav.conf <<'EOF'
|
||||||
|
check process clamd with pidfile /run/clamav/clamd.pid
|
||||||
|
start program = "/bin/systemctl start clamav-daemon"
|
||||||
|
stop program = "/bin/systemctl stop clamav-daemon"
|
||||||
|
if failed unixsocket /run/clamav/clamd.ctl for 3 cycles then restart
|
||||||
|
if 5 restarts within 5 cycles then timeout
|
||||||
|
EOF
|
||||||
|
else
|
||||||
|
rm -f /etc/monit/conf.d/60-clamav.conf || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 70 – Fail2Ban (optional, standardmäßig aktiv)
|
||||||
|
if [[ "$FAIL2BAN_ENABLE" = "1" ]]; then
|
||||||
|
cat >/etc/monit/conf.d/70-fail2ban.conf <<'EOF'
|
||||||
|
check process fail2ban with pidfile /run/fail2ban/fail2ban.pid
|
||||||
|
start program = "/bin/systemctl start fail2ban"
|
||||||
|
stop program = "/bin/systemctl stop fail2ban"
|
||||||
|
if 5 restarts within 5 cycles then alert
|
||||||
|
EOF
|
||||||
|
else
|
||||||
|
rm -f /etc/monit/conf.d/70-fail2ban.conf || true
|
||||||
|
fi
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# Monit neu laden
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
monit -t
|
||||||
|
systemctl reload monit || systemctl restart monit
|
||||||
|
systemctl status monit --no-pager || true
|
||||||
|
log "[✓] WoltGuard aktiv."
|
||||||
|
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#set -euo pipefail
|
||||||
|
#source ./lib.sh
|
||||||
|
#
|
||||||
|
#log "WoltGuard (Monit + Self-Heal) einrichten …"
|
||||||
|
#
|
||||||
|
#set +u
|
||||||
|
#[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
|
||||||
|
#set -u
|
||||||
|
#CLAMAV_ENABLE="${CLAMAV_ENABLE:-0}"
|
||||||
|
#OPENDMARC_ENABLE="${OPENDMARC_ENABLE:-0}"
|
||||||
|
#FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}"
|
||||||
|
#
|
||||||
|
## Pakete sicherstellen
|
||||||
|
#command -v monit >/dev/null || { apt-get update -qq; apt-get install -y monit; }
|
||||||
|
#systemctl enable --now monit
|
||||||
|
#
|
||||||
|
## Helper-Skripte
|
||||||
|
#install -d -m 0755 /usr/local/sbin
|
||||||
|
#cat >/usr/local/sbin/mailwolt-redis-ping.sh <<'EOSH'
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#set -euo pipefail
|
||||||
|
#PASS=""
|
||||||
|
#[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env || true
|
||||||
|
#if command -v redis-cli >/dev/null 2>&1; then
|
||||||
|
# [[ -n "${REDIS_PASS:-}" ]] \
|
||||||
|
# && redis-cli -h 127.0.0.1 -p 6379 -a "$REDIS_PASS" ping | grep -q PONG \
|
||||||
|
# || redis-cli -h 127.0.0.1 -p 6379 ping | grep -q PONG
|
||||||
|
#else
|
||||||
|
# exit 1
|
||||||
|
#fi
|
||||||
|
#EOSH
|
||||||
|
#chmod 0755 /usr/local/sbin/mailwolt-redis-ping.sh
|
||||||
|
#
|
||||||
|
#cat >/usr/local/sbin/mailwolt-rspamd-heal.sh <<'EOSH'
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#set -euo pipefail
|
||||||
|
#
|
||||||
|
#REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
|
||||||
|
#REDIS_PORT="${REDIS_PORT:-6379}"
|
||||||
|
#REDIS_PASSWORD="${REDIS_PASSWORD:-}"
|
||||||
|
#
|
||||||
|
#INSTALLER_ENV="/etc/mailwolt/installer.env"
|
||||||
|
#APP_ENV="/var/www/mailwolt/.env"
|
||||||
|
#REDIS_CLI="$(command -v redis-cli || true)"
|
||||||
|
#SYSTEMCTL="$(command -v systemctl || true)"
|
||||||
|
#RSPAMD_SERVICE="rspamd"
|
||||||
|
#
|
||||||
|
#if [ -r "$INSTALLER_ENV" ]; then . "$INSTALLER_ENV"; fi
|
||||||
|
#if [ -z "${REDIS_PASSWORD}" ] && [ -r "$APP_ENV" ]; then
|
||||||
|
# REDIS_PASSWORD="$(grep -E '^REDIS_PASSWORD=' "$APP_ENV" | head -n1 | cut -d= -f2- || true)"
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
#if [ -n "$REDIS_CLI" ]; then
|
||||||
|
# echo "[INFO] Prüfe Redis Verbindung..."
|
||||||
|
# if [ -n "${REDIS_PASSWORD}" ]; then
|
||||||
|
# if ! "$REDIS_CLI" -h "$REDIS_HOST" -p "$REDIS_PORT" -a "$REDIS_PASSWORD" ping | grep -q '^PONG$'; then
|
||||||
|
# echo "[WARN] Redis antwortet nicht oder Passwort falsch!"
|
||||||
|
# else
|
||||||
|
# echo "[OK] Redis antwortet (auth ok)."
|
||||||
|
# fi
|
||||||
|
# else
|
||||||
|
# if ! "$REDIS_CLI" -h "$REDIS_HOST" -p "$REDIS_PORT" ping | grep -q '^PONG$'; then
|
||||||
|
# echo "[WARN] Redis antwortet nicht (ohne Passwort)."
|
||||||
|
# else
|
||||||
|
# echo "[OK] Redis antwortet (kein Passwort)."
|
||||||
|
# fi
|
||||||
|
# fi
|
||||||
|
#else
|
||||||
|
# echo "[WARN] redis-cli nicht gefunden – überspringe Test."
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
#echo "[INFO] Prüfe Rspamd Socket & Verzeichnis..."
|
||||||
|
#install -d -m 0755 -o _rspamd -g _rspamd /run/rspamd || true
|
||||||
|
#[ -S /var/lib/rspamd/rspamd.sock ] && rm -f /var/lib/rspamd/rspamd.sock || true
|
||||||
|
#
|
||||||
|
#echo "[INFO] Starte Rspamd neu..."
|
||||||
|
#if [ -n "$SYSTEMCTL" ]; then
|
||||||
|
# "$SYSTEMCTL" restart "$RSPAMD_SERVICE"
|
||||||
|
# echo "[OK] Rspamd erfolgreich neu gestartet."
|
||||||
|
#else
|
||||||
|
# echo "[ERROR] systemctl nicht gefunden – kein Neustart möglich."
|
||||||
|
# exit 1
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
#echo "[INFO] Healthcheck (Port 11334)..."
|
||||||
|
#sleep 3
|
||||||
|
#if ss -tln | grep -q ':11334'; then
|
||||||
|
# echo "[OK] Rspamd Controller läuft auf Port 11334."
|
||||||
|
#else
|
||||||
|
# echo "[WARN] Rspamd Controller Port 11334 nicht erreichbar."
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
#echo "[DONE] Mailwolt Rspamd-Heal abgeschlossen."
|
||||||
|
#exit 0
|
||||||
|
#EOSH
|
||||||
|
#chmod 0755 /usr/local/sbin/mailwolt-rspamd-heal.sh
|
||||||
|
#
|
||||||
|
## WoltGuard Wrapper + Unit
|
||||||
|
#cat >/usr/local/bin/woltguard <<'EOSH'
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#set -euo pipefail
|
||||||
|
#case "${1:-status}" in
|
||||||
|
# start) systemctl enable --now monit ;;
|
||||||
|
# stop) systemctl stop monit ;;
|
||||||
|
# status) monit summary || systemctl status monit || true ;;
|
||||||
|
# heal) monit reload || true; sleep 1; monit restart all || true ;;
|
||||||
|
# monitor) monit monitor all || true ;;
|
||||||
|
# unmonitor) monit unmonitor all || true ;;
|
||||||
|
# *) echo "Usage: woltguard {start|stop|status|heal|monitor|unmonitor}"; exit 2;;
|
||||||
|
#esac
|
||||||
|
#EOSH
|
||||||
|
#chmod 0755 /usr/local/bin/woltguard
|
||||||
|
#
|
||||||
|
#cat >/etc/systemd/system/woltguard.service <<'EOF'
|
||||||
|
#[Unit]
|
||||||
|
#Description=WoltGuard – Self-Healing Monitor for MailWolt
|
||||||
|
#After=network.target
|
||||||
|
#[Service]
|
||||||
|
#Type=oneshot
|
||||||
|
#ExecStart=/usr/local/bin/woltguard start
|
||||||
|
#ExecStop=/usr/local/bin/woltguard stop
|
||||||
|
#RemainAfterExit=yes
|
||||||
|
#[Install]
|
||||||
|
#WantedBy=multi-user.target
|
||||||
|
#EOF
|
||||||
|
#systemctl daemon-reload
|
||||||
|
#systemctl enable --now woltguard
|
||||||
|
#
|
||||||
|
## Monit Basis + include
|
||||||
|
#sed -i 's/^set daemon .*/set daemon 30/' /etc/monit/monitrc || true
|
||||||
|
#grep -q 'include /etc/monit/conf.d/*' /etc/monit/monitrc || echo 'include /etc/monit/conf.d/*' >>/etc/monit/monitrc
|
||||||
|
#install -d -m 0755 /etc/monit/conf.d
|
||||||
|
#
|
||||||
|
## Checks
|
||||||
|
#cat >/etc/monit/conf.d/postfix.conf <<'EOF'
|
||||||
|
#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 tcpssl then restart
|
||||||
|
# if failed port 587 type tcp then restart
|
||||||
|
# if 5 restarts within 5 cycles then alert
|
||||||
|
#EOF
|
||||||
|
#
|
||||||
|
#cat >/etc/monit/conf.d/dovecot.conf <<'EOF'
|
||||||
|
#check process dovecot with pidfile /run/dovecot/master.pid
|
||||||
|
# start program = "/bin/systemctl start dovecot"
|
||||||
|
# stop program = "/bin/systemctl stop dovecot"
|
||||||
|
# if failed port 993 type tcpssl for 2 cycles then restart
|
||||||
|
# if failed port 24 protocol lmtp for 2 cycles then restart
|
||||||
|
# if 5 restarts within 5 cycles then alert
|
||||||
|
#EOF
|
||||||
|
#
|
||||||
|
#cat >/etc/monit/conf.d/nginx.conf <<'EOF'
|
||||||
|
#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 tcpssl then restart
|
||||||
|
# if 5 restarts within 5 cycles then alert
|
||||||
|
#EOF
|
||||||
|
#
|
||||||
|
#cat >/etc/monit/conf.d/redis.conf <<'EOF'
|
||||||
|
#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 host 127.0.0.1 port 6379 for 2 cycles then restart
|
||||||
|
# if 5 restarts within 5 cycles then alert
|
||||||
|
#
|
||||||
|
#check program redis_ping path "/usr/local/sbin/mailwolt-redis-ping.sh"
|
||||||
|
# if status != 0 for 2 cycles then exec "/bin/systemctl restart redis-server"
|
||||||
|
#EOF
|
||||||
|
#
|
||||||
|
#cat >/etc/monit/conf.d/rspamd.conf <<'EOF'
|
||||||
|
#check process rspamd with pidfile /run/rspamd/rspamd.pid
|
||||||
|
# start program = "/bin/systemctl start rspamd"
|
||||||
|
# stop program = "/bin/systemctl stop rspamd"
|
||||||
|
# if failed port 11333 for 2 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh"
|
||||||
|
# if failed port 11334 for 2 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh"
|
||||||
|
# if 5 restarts within 5 cycles then alert
|
||||||
|
#EOF
|
||||||
|
#
|
||||||
|
#cat >/etc/monit/conf.d/opendkim.conf <<'EOF'
|
||||||
|
#check process opendkim with pidfile /run/opendkim/opendkim.pid
|
||||||
|
# start program = "/bin/systemctl start opendkim"
|
||||||
|
# stop program = "/bin/systemctl stop opendkim"
|
||||||
|
# if failed host 127.0.0.1 port 8891 type tcp for 2 cycles then restart
|
||||||
|
# if 5 restarts within 5 cycles then alert
|
||||||
|
#EOF
|
||||||
|
#
|
||||||
|
## optional: OpenDMARC
|
||||||
|
#if [[ "$OPENDMARC_ENABLE" = "1" ]]; then
|
||||||
|
# cat >/etc/monit/conf.d/opendmarc.conf <<'EOF'
|
||||||
|
#check process opendmarc with pidfile /run/opendmarc/opendmarc.pid
|
||||||
|
# start program = "/bin/systemctl start opendmarc"
|
||||||
|
# stop program = "/bin/systemctl stop opendmarc"
|
||||||
|
# if 5 restarts within 5 cycles then alert
|
||||||
|
#EOF
|
||||||
|
#else
|
||||||
|
# rm -f /etc/monit/conf.d/opendmarc.conf || true
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
## optional: ClamAV
|
||||||
|
#if [[ "$CLAMAV_ENABLE" = "1" ]]; then
|
||||||
|
# cat >/etc/monit/conf.d/clamav.conf <<'EOF'
|
||||||
|
#check process clamd with pidfile /run/clamav/clamd.pid
|
||||||
|
# start program = "/bin/systemctl start clamav-daemon"
|
||||||
|
# stop program = "/bin/systemctl stop clamav-daemon"
|
||||||
|
# if failed unixsocket /run/clamav/clamd.ctl then restart
|
||||||
|
# if 5 restarts within 5 cycles then alert
|
||||||
|
#EOF
|
||||||
|
#else
|
||||||
|
# rm -f /etc/monit/conf.d/clamav.conf || true
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
## optional: Fail2Ban
|
||||||
|
#if [[ "$FAIL2BAN_ENABLE" = "1" ]]; then
|
||||||
|
# cat >/etc/monit/conf.d/fail2ban.conf <<'EOF'
|
||||||
|
#check process fail2ban with pidfile /run/fail2ban/fail2ban.pid
|
||||||
|
# start program = "/bin/systemctl start fail2ban"
|
||||||
|
# stop program = "/bin/systemctl stop fail2ban"
|
||||||
|
# if 5 restarts within 5 cycles then alert
|
||||||
|
#EOF
|
||||||
|
#else
|
||||||
|
# rm -f /etc/monit/conf.d/fail2ban.conf || true
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
#monit -t
|
||||||
|
#systemctl reload monit || systemctl restart monit
|
||||||
|
#systemctl status monit --no-pager || true
|
||||||
|
#log "[✓] WoltGuard aktiv."
|
||||||
|
|
@ -0,0 +1,439 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Flags laden (falls vorhanden)
|
||||||
|
INSTALLER_ENV="/etc/mailwolt/installer.env"
|
||||||
|
: "${CLAMAV_ENABLE:=}"; : "${OPENDMARC_ENABLE:=}"; : "${FAIL2BAN_ENABLE:=}"; : "${MONIT_HTTP:=}"
|
||||||
|
if [[ -z "${CLAMAV_ENABLE}${OPENDMARC_ENABLE}${FAIL2BAN_ENABLE}" && -r "$INSTALLER_ENV" ]]; then
|
||||||
|
. "$INSTALLER_ENV"
|
||||||
|
fi
|
||||||
|
CLAMAV_ENABLE="${CLAMAV_ENABLE:-1}"
|
||||||
|
OPENDMARC_ENABLE="${OPENDMARC_ENABLE:-1}"
|
||||||
|
FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}"
|
||||||
|
MONIT_HTTP="${MONIT_HTTP:-1}"
|
||||||
|
|
||||||
|
# ── Monit so konfigurieren, dass NUR monitrc.d/* geladen wird ────────────────
|
||||||
|
install -d -m 0755 /etc/monit/monitrc.d
|
||||||
|
install -d -m 0755 /etc/monit/conf.d # passiver Ablageort (NICHT includiert)
|
||||||
|
|
||||||
|
# Poll-Intervall (30s)
|
||||||
|
sed -i 's/^set daemon .*/set daemon 30/' /etc/monit/monitrc || true
|
||||||
|
# alle alten include-Zeilen raus und monitrc.d setzen
|
||||||
|
sed -i 's|^#\?\s*include .*$||g' /etc/monit/monitrc
|
||||||
|
grep -q '^include /etc/monit/monitrc.d/\*' /etc/monit/monitrc \
|
||||||
|
|| echo 'include /etc/monit/monitrc.d/*' >> /etc/monit/monitrc
|
||||||
|
|
||||||
|
# Optional: HTTP-UI nur einschalten, wenn explizit gewünscht
|
||||||
|
if [[ "$MONIT_HTTP" = "1" ]]; then
|
||||||
|
grep -q '^set httpd port 2812' /etc/monit/monitrc || cat >>/etc/monit/monitrc <<'HTTP'
|
||||||
|
set httpd port 2812 and
|
||||||
|
use address localhost
|
||||||
|
allow localhost
|
||||||
|
HTTP
|
||||||
|
fi
|
||||||
|
|
||||||
|
# KEIN Löschen mehr der Dateien – wir verschieben je nach Status
|
||||||
|
# (vorher stand hier rm -rf /etc/monit/monitrc.d/* und rm -f /etc/monit/conf.d/*.conf)
|
||||||
|
|
||||||
|
# ── Helper-Skripte ──────────────────────────────────────────────────────────
|
||||||
|
install -d -m 0755 /usr/local/sbin
|
||||||
|
|
||||||
|
# Redis-Ping (Password: REDIS_PASSWORD aus installer.env oder .env)
|
||||||
|
cat >/usr/local/sbin/mailwolt-redis-ping.sh <<'EOSH'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
INSTALLER_ENV="/etc/mailwolt/installer.env"
|
||||||
|
APP_ENV="/var/www/mailwolt/.env"
|
||||||
|
REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
|
||||||
|
REDIS_PORT="${REDIS_PORT:-6379}"
|
||||||
|
REDIS_PASSWORD="${REDIS_PASSWORD:-}"
|
||||||
|
REDIS_PASS="${REDIS_PASS:-}"
|
||||||
|
|
||||||
|
[[ -r "$INSTALLER_ENV" ]] && . "$INSTALLER_ENV" || true
|
||||||
|
if [[ -r "$APP_ENV" ]]; then
|
||||||
|
[[ -z "${REDIS_HOST}" ]] && REDIS_HOST="$(grep -m1 '^REDIS_HOST=' "$APP_ENV" | cut -d= -f2- || true)"
|
||||||
|
[[ -z "${REDIS_PORT}" ]] && REDIS_PORT="$(grep -m1 '^REDIS_PORT=' "$APP_ENV" | cut -d= -f2- || true)"
|
||||||
|
[[ -z "${REDIS_PASSWORD}" ]] && REDIS_PASSWORD="$(grep -m1 '^REDIS_PASSWORD=' "$APP_ENV" | cut -d= -f2- || true)"
|
||||||
|
fi
|
||||||
|
[[ -z "${REDIS_PASSWORD}" && -n "${REDIS_PASS}" ]] && REDIS_PASSWORD="$REDIS_PASS"
|
||||||
|
|
||||||
|
strip(){ printf '%s' "$1" | sed -E 's/^"(.*)"$/\1/; s/^'"'"'(.*)'"'"'$/\1/'; }
|
||||||
|
REDIS_HOST="$(strip "${REDIS_HOST:-}")"
|
||||||
|
REDIS_PORT="$(strip "${REDIS_PORT:-}")"
|
||||||
|
REDIS_PASSWORD="$(strip "${REDIS_PASSWORD:-}")"
|
||||||
|
|
||||||
|
command -v redis-cli >/dev/null 2>&1 || exit 1
|
||||||
|
BASE=(timeout 2 redis-cli --no-auth-warning --raw -h "$REDIS_HOST" -p "$REDIS_PORT")
|
||||||
|
[[ -n "$REDIS_PASSWORD" ]] && CMD=("${BASE[@]}" -a "$REDIS_PASSWORD" ping) || CMD=("${BASE[@]}" ping)
|
||||||
|
[[ "$("${CMD[@]}" 2>/dev/null || true)" == "PONG" ]]
|
||||||
|
EOSH
|
||||||
|
chmod 0755 /usr/local/sbin/mailwolt-redis-ping.sh
|
||||||
|
|
||||||
|
# Rspamd-Heal (Socke aufräumen, restart, Mini-Port-Check)
|
||||||
|
cat >/usr/local/sbin/mailwolt-rspamd-heal.sh <<'EOSH'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
INSTALLER_ENV="/etc/mailwolt/installer.env"
|
||||||
|
APP_ENV="/var/www/mailwolt/.env"
|
||||||
|
|
||||||
|
REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
|
||||||
|
REDIS_PORT="${REDIS_PORT:-6379}"
|
||||||
|
REDIS_PASS="${REDIS_PASS:-}"
|
||||||
|
|
||||||
|
[[ -r "$INSTALLER_ENV" ]] && . "$INSTALLER_ENV"
|
||||||
|
if [[ -z "${REDIS_PASS}" && -r "$APP_ENV" ]]; then
|
||||||
|
REDIS_PASS="$(grep -E '^REDIS_PASS=' "$APP_ENV" | head -n1 | cut -d= -f2- || true)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Rspamd Runtime fixen
|
||||||
|
install -d -m 0755 -o _rspamd -g _rspamd /run/rspamd || true
|
||||||
|
[[ -S /var/lib/rspamd/rspamd.sock ]] && rm -f /var/lib/rspamd/rspamd.sock || true
|
||||||
|
|
||||||
|
echo "$(date '+%F %T') heal run" >> /var/log/rspamd-heal.log
|
||||||
|
|
||||||
|
# Neustart
|
||||||
|
systemctl restart rspamd
|
||||||
|
|
||||||
|
# Mini-Healthcheck
|
||||||
|
sleep 2
|
||||||
|
ss -tln | grep -q ':11334' || echo "[WARN] Rspamd Controller Port 11334 nicht sichtbar"
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
EOSH
|
||||||
|
chmod 0755 /usr/local/sbin/mailwolt-rspamd-heal.sh
|
||||||
|
|
||||||
|
# ── Monit-Checks (nummeriert) – fixe Dienste immer aktiv ────────────────────
|
||||||
|
# 10 – Redis
|
||||||
|
cat >/etc/monit/monitrc.d/10-redis.conf <<'EOF'
|
||||||
|
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 host 127.0.0.1 port 6379 for 2 cycles then restart
|
||||||
|
if 5 restarts within 5 cycles then alert
|
||||||
|
|
||||||
|
check program redis_ping path "/usr/local/sbin/mailwolt-redis-ping.sh"
|
||||||
|
if status != 0 for 2 cycles then exec "/bin/systemctl restart redis-server"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 20 – Rspamd (robust via process-matching + Heal)
|
||||||
|
cat >/etc/monit/monitrc.d/20-rspamd.conf <<'EOF'
|
||||||
|
check process rspamd matching "rspamd: main process"
|
||||||
|
start program = "/bin/systemctl start rspamd" with timeout 120 seconds
|
||||||
|
stop program = "/bin/systemctl stop rspamd"
|
||||||
|
depends on redis
|
||||||
|
if failed host 127.0.0.1 port 11333 for 3 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh"
|
||||||
|
if failed host 127.0.0.1 port 11334 for 3 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh"
|
||||||
|
if does not exist for 2 cycles then restart
|
||||||
|
if 5 restarts within 10 cycles then unmonitor
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 30 – Postfix
|
||||||
|
cat >/etc/monit/monitrc.d/30-postfix.conf <<'EOF'
|
||||||
|
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 host 127.0.0.1 port 25 type tcp with timeout 15 seconds for 3 cycles then restart
|
||||||
|
if failed host 127.0.0.1 port 465 type tcpssl with timeout 10 seconds then restart
|
||||||
|
if failed host 127.0.0.1 port 587 type tcp with timeout 10 seconds then restart
|
||||||
|
if 5 restarts within 5 cycles then alert
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 30 – Dovecot (IMAPS; LMTP oft Unix-Socket → kein TCP-Fehlalarm)
|
||||||
|
cat >/etc/monit/monitrc.d/30-dovecot.conf <<'EOF'
|
||||||
|
check process dovecot with pidfile /run/dovecot/master.pid
|
||||||
|
start program = "/bin/systemctl start dovecot"
|
||||||
|
stop program = "/bin/systemctl stop dovecot"
|
||||||
|
if failed port 993 type tcpssl for 3 cycles then restart
|
||||||
|
if 5 restarts within 10 cycles then alert
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 40 – Nginx
|
||||||
|
cat >/etc/monit/monitrc.d/40-nginx.conf <<'EOF'
|
||||||
|
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 tcpssl then restart
|
||||||
|
if 5 restarts within 5 cycles then alert
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 50 – OpenDKIM
|
||||||
|
cat >/etc/monit/monitrc.d/50-opendkim.conf <<'EOF'
|
||||||
|
check process opendkim with pidfile /run/opendkim/opendkim.pid
|
||||||
|
start program = "/bin/systemctl start opendkim"
|
||||||
|
stop program = "/bin/systemctl stop opendkim"
|
||||||
|
if failed host 127.0.0.1 port 8891 type tcp for 2 cycles then restart
|
||||||
|
if 5 restarts within 5 cycles then alert
|
||||||
|
EOF
|
||||||
|
|
||||||
|
move_monit_conf() {
|
||||||
|
local name="$1" # z.B. 55-opendmarc
|
||||||
|
local enabled="$2" # "0" oder "1"
|
||||||
|
local src="/etc/monit/conf.d/${name}.conf"
|
||||||
|
local dst="/etc/monit/monitrc.d/${name}.conf"
|
||||||
|
|
||||||
|
mkdir -p /etc/monit/conf.d /etc/monit/monitrc.d
|
||||||
|
|
||||||
|
# Falls Datei nirgends existiert → in conf.d anlegen (lesbare Quelle)
|
||||||
|
if [[ ! -f "$src" && ! -f "$dst" ]]; then
|
||||||
|
cat >"$src" <<'EOF_PAYLOAD'
|
||||||
|
__PAYLOAD__
|
||||||
|
EOF_PAYLOAD
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$enabled" = "1" ]]; then
|
||||||
|
# Aktiv: in monitrc.d haben
|
||||||
|
if [[ -f "$src" && ! -f "$dst" ]]; then
|
||||||
|
mv -f "$src" "$dst"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Inaktiv: in conf.d haben
|
||||||
|
if [[ -f "$dst" && ! -f "$src" ]]; then
|
||||||
|
mv -f "$dst" "$src"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
move_monit_conf "55-opendmarc" "${OPENDMARC_ENABLE:-0}" <<'EOF'
|
||||||
|
check process opendmarc with pidfile /run/opendmarc/opendmarc.pid
|
||||||
|
start program = "/bin/systemctl start opendmarc"
|
||||||
|
stop program = "/bin/systemctl stop opendmarc"
|
||||||
|
if 5 restarts within 5 cycles then alert
|
||||||
|
EOF
|
||||||
|
|
||||||
|
move_monit_conf "60-clamav" "${CLAMAV_ENABLE:-0}" <<'EOF'
|
||||||
|
check process clamd matching "clamd"
|
||||||
|
start program = "/bin/systemctl start clamav-daemon"
|
||||||
|
stop program = "/bin/systemctl stop clamav-daemon"
|
||||||
|
if failed unixsocket /run/clamav/clamd.ctl for 3 cycles then restart
|
||||||
|
if 5 restarts within 10 cycles then unmonitor
|
||||||
|
EOF
|
||||||
|
|
||||||
|
move_monit_conf "70-fail2ban" "${FAIL2BAN_ENABLE:-0}" <<'EOF'
|
||||||
|
check process fail2ban with pidfile /run/fail2ban/fail2ban.pid
|
||||||
|
start program = "/bin/systemctl start fail2ban"
|
||||||
|
stop program = "/bin/systemctl stop fail2ban"
|
||||||
|
if 5 restarts within 5 cycles then alert
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# ── Monit neu laden ─────────────────────────────────────────────────────────
|
||||||
|
monit -t
|
||||||
|
systemctl reload monit || systemctl restart monit
|
||||||
|
|
||||||
|
# Optionaler Sichttest (CLI funktioniert auch ohne HTTP-UI)
|
||||||
|
# sleep 2
|
||||||
|
# monit summary || true
|
||||||
|
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#set -euo pipefail
|
||||||
|
#
|
||||||
|
## Flags laden (falls vorhanden)
|
||||||
|
#INSTALLER_ENV="/etc/mailwolt/installer.env"
|
||||||
|
#: "${CLAMAV_ENABLE:=}"; : "${OPENDMARC_ENABLE:=}"; : "${FAIL2BAN_ENABLE:=}"; : "${MONIT_HTTP:=}"
|
||||||
|
#if [[ -z "${CLAMAV_ENABLE}${OPENDMARC_ENABLE}${FAIL2BAN_ENABLE}" && -r "$INSTALLER_ENV" ]]; then
|
||||||
|
# . "$INSTALLER_ENV"
|
||||||
|
#fi
|
||||||
|
#CLAMAV_ENABLE="${CLAMAV_ENABLE:-1}"
|
||||||
|
#OPENDMARC_ENABLE="${OPENDMARC_ENABLE:-1}"
|
||||||
|
#FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}"
|
||||||
|
#MONIT_HTTP="${MONIT_HTTP:-1}"
|
||||||
|
#
|
||||||
|
## ── Monit so konfigurieren, dass NUR monitrc.d/* geladen wird ────────────────
|
||||||
|
#install -d -m 0755 /etc/monit/monitrc.d
|
||||||
|
## Poll-Intervall (30s)
|
||||||
|
#sed -i 's/^set daemon .*/set daemon 30/' /etc/monit/monitrc || true
|
||||||
|
## alle alten include-Zeilen raus und monitrc.d setzen
|
||||||
|
#sed -i 's|^#\?\s*include .*$||g' /etc/monit/monitrc
|
||||||
|
#grep -q '^include /etc/monit/monitrc.d/\*' /etc/monit/monitrc \
|
||||||
|
# || echo 'include /etc/monit/monitrc.d/*' >> /etc/monit/monitrc
|
||||||
|
#
|
||||||
|
## Optional: HTTP-UI nur einschalten, wenn explizit gewünscht
|
||||||
|
#if [[ "$MONIT_HTTP" = "1" ]]; then
|
||||||
|
# grep -q '^set httpd port 2812' /etc/monit/monitrc || cat >>/etc/monit/monitrc <<'HTTP'
|
||||||
|
#set httpd port 2812 and
|
||||||
|
# use address localhost
|
||||||
|
# allow localhost
|
||||||
|
#HTTP
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
#sudo mkdir -p /etc/monit/monitrc.d
|
||||||
|
#sudo rm -rf /etc/monit/monitrc.d/* 2>/dev/null || true
|
||||||
|
#sudo rm -f /etc/monit/conf.d/*.conf 2>/dev/null || true
|
||||||
|
#
|
||||||
|
## ── Helper-Skripte ──────────────────────────────────────────────────────────
|
||||||
|
#install -d -m 0755 /usr/local/sbin
|
||||||
|
#
|
||||||
|
## Redis-Ping (Password: REDIS_PASSWORD aus installer.env oder .env)
|
||||||
|
#cat >/usr/local/sbin/mailwolt-redis-ping.sh <<'EOSH'
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#set -euo pipefail
|
||||||
|
#INSTALLER_ENV="/etc/mailwolt/installer.env"
|
||||||
|
#APP_ENV="/var/www/mailwolt/.env"
|
||||||
|
#REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
|
||||||
|
#REDIS_PORT="${REDIS_PORT:-6379}"
|
||||||
|
#REDIS_PASSWORD="${REDIS_PASSWORD:-}"
|
||||||
|
#REDIS_PASS="${REDIS_PASS:-}"
|
||||||
|
#
|
||||||
|
#[[ -r "$INSTALLER_ENV" ]] && . "$INSTALLER_ENV" || true
|
||||||
|
#if [[ -r "$APP_ENV" ]]; then
|
||||||
|
# [[ -z "${REDIS_HOST}" ]] && REDIS_HOST="$(grep -m1 '^REDIS_HOST=' "$APP_ENV" | cut -d= -f2- || true)"
|
||||||
|
# [[ -z "${REDIS_PORT}" ]] && REDIS_PORT="$(grep -m1 '^REDIS_PORT=' "$APP_ENV" | cut -d= -f2- || true)"
|
||||||
|
# [[ -z "${REDIS_PASSWORD}" ]] && REDIS_PASSWORD="$(grep -m1 '^REDIS_PASSWORD=' "$APP_ENV" | cut -d= -f2- || true)"
|
||||||
|
#fi
|
||||||
|
#[[ -z "${REDIS_PASSWORD}" && -n "${REDIS_PASS}" ]] && REDIS_PASSWORD="$REDIS_PASS"
|
||||||
|
#
|
||||||
|
#strip(){ printf '%s' "$1" | sed -E 's/^"(.*)"$/\1/; s/^'"'"'(.*)'"'"'$/\1/'; }
|
||||||
|
#REDIS_HOST="$(strip "${REDIS_HOST:-}")"
|
||||||
|
#REDIS_PORT="$(strip "${REDIS_PORT:-}")"
|
||||||
|
#REDIS_PASSWORD="$(strip "${REDIS_PASSWORD:-}")"
|
||||||
|
#
|
||||||
|
#command -v redis-cli >/dev/null 2>&1 || exit 1
|
||||||
|
#BASE=(timeout 2 redis-cli --no-auth-warning --raw -h "$REDIS_HOST" -p "$REDIS_PORT")
|
||||||
|
#[[ -n "$REDIS_PASSWORD" ]] && CMD=("${BASE[@]}" -a "$REDIS_PASSWORD" ping) || CMD=("${BASE[@]}" ping)
|
||||||
|
#[[ "$("${CMD[@]}" 2>/dev/null || true)" == "PONG" ]]
|
||||||
|
#EOSH
|
||||||
|
#chmod 0755 /usr/local/sbin/mailwolt-redis-ping.sh
|
||||||
|
#
|
||||||
|
## Rspamd-Heal (Socke aufräumen, restart, Mini-Port-Check)
|
||||||
|
#cat >/usr/local/sbin/mailwolt-rspamd-heal.sh <<'EOSH'
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#set -euo pipefail
|
||||||
|
#
|
||||||
|
#INSTALLER_ENV="/etc/mailwolt/installer.env"
|
||||||
|
#APP_ENV="/var/www/mailwolt/.env"
|
||||||
|
#
|
||||||
|
#REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
|
||||||
|
#REDIS_PORT="${REDIS_PORT:-6379}"
|
||||||
|
#REDIS_PASS="${REDIS_PASS:-}"
|
||||||
|
#
|
||||||
|
#[[ -r "$INSTALLER_ENV" ]] && . "$INSTALLER_ENV"
|
||||||
|
#if [[ -z "${REDIS_PASS}" && -r "$APP_ENV" ]]; then
|
||||||
|
# REDIS_PASS="$(grep -E '^REDIS_PASS=' "$APP_ENV" | head -n1 | cut -d= -f2- || true)"
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
## Rspamd Runtime fixen
|
||||||
|
#install -d -m 0755 -o _rspamd -g _rspamd /run/rspamd || true
|
||||||
|
#[[ -S /var/lib/rspamd/rspamd.sock ]] && rm -f /var/lib/rspamd/rspamd.sock || true
|
||||||
|
#
|
||||||
|
#echo "$(date '+%F %T') heal run" >> /var/log/rspamd-heal.log
|
||||||
|
#
|
||||||
|
## Neustart
|
||||||
|
#systemctl restart rspamd
|
||||||
|
#
|
||||||
|
## Mini-Healthcheck
|
||||||
|
#sleep 2
|
||||||
|
#ss -tln | grep -q ':11334' || echo "[WARN] Rspamd Controller Port 11334 nicht sichtbar"
|
||||||
|
#
|
||||||
|
#exit 0
|
||||||
|
#EOSH
|
||||||
|
#chmod 0755 /usr/local/sbin/mailwolt-rspamd-heal.sh
|
||||||
|
#
|
||||||
|
## ── Monit-Checks (nummeriert) ───────────────────────────────────────────────
|
||||||
|
## 10 – Redis
|
||||||
|
#cat >/etc/monit/monitrc.d/10-redis.conf <<'EOF'
|
||||||
|
#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 host 127.0.0.1 port 6379 for 2 cycles then restart
|
||||||
|
# if 5 restarts within 5 cycles then alert
|
||||||
|
#
|
||||||
|
#check program redis_ping path "/usr/local/sbin/mailwolt-redis-ping.sh"
|
||||||
|
# if status != 0 for 2 cycles then exec "/bin/systemctl restart redis-server"
|
||||||
|
#EOF
|
||||||
|
#
|
||||||
|
## 20 – Rspamd (robust via process-matching + Heal)
|
||||||
|
#cat >/etc/monit/monitrc.d/20-rspamd.conf <<'EOF'
|
||||||
|
#check process rspamd matching "rspamd: main process"
|
||||||
|
# start program = "/bin/systemctl start rspamd" with timeout 120 seconds
|
||||||
|
# stop program = "/bin/systemctl stop rspamd"
|
||||||
|
# depends on redis
|
||||||
|
# if failed host 127.0.0.1 port 11333 for 3 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh"
|
||||||
|
# if failed host 127.0.0.1 port 11334 for 3 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh"
|
||||||
|
# if does not exist for 2 cycles then restart
|
||||||
|
# if 5 restarts within 10 cycles then unmonitor
|
||||||
|
#EOF
|
||||||
|
#
|
||||||
|
## 30 – Postfix
|
||||||
|
#cat >/etc/monit/monitrc.d/30-postfix.conf <<'EOF'
|
||||||
|
#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 host 127.0.0.1 port 25 type tcp with timeout 15 seconds for 3 cycles then restart
|
||||||
|
# if failed host 127.0.0.1 port 465 type tcpssl with timeout 10 seconds then restart
|
||||||
|
# if failed host 127.0.0.1 port 587 type tcp with timeout 10 seconds then restart
|
||||||
|
# if 5 restarts within 5 cycles then alert
|
||||||
|
#EOF
|
||||||
|
#
|
||||||
|
## 30 – Dovecot (IMAPS; LMTP oft Unix-Socket → kein TCP-Fehlalarm)
|
||||||
|
#cat >/etc/monit/monitrc.d/30-dovecot.conf <<'EOF'
|
||||||
|
#check process dovecot with pidfile /run/dovecot/master.pid
|
||||||
|
# start program = "/bin/systemctl start dovecot"
|
||||||
|
# stop program = "/bin/systemctl stop dovecot"
|
||||||
|
# if failed port 993 type tcpssl for 3 cycles then restart
|
||||||
|
# if 5 restarts within 10 cycles then alert
|
||||||
|
#EOF
|
||||||
|
#
|
||||||
|
## 40 – Nginx
|
||||||
|
#cat >/etc/monit/monitrc.d/40-nginx.conf <<'EOF'
|
||||||
|
#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 tcpssl then restart
|
||||||
|
# if 5 restarts within 5 cycles then alert
|
||||||
|
#EOF
|
||||||
|
#
|
||||||
|
## 50 – OpenDKIM
|
||||||
|
#cat >/etc/monit/monitrc.d/50-opendkim.conf <<'EOF'
|
||||||
|
#check process opendkim with pidfile /run/opendkim/opendkim.pid
|
||||||
|
# start program = "/bin/systemctl start opendkim"
|
||||||
|
# stop program = "/bin/systemctl stop opendkim"
|
||||||
|
# if failed host 127.0.0.1 port 8891 type tcp for 2 cycles then restart
|
||||||
|
# if 5 restarts within 5 cycles then alert
|
||||||
|
#EOF
|
||||||
|
#
|
||||||
|
## 55 – OpenDMARC (optional)
|
||||||
|
#if [[ "$OPENDMARC_ENABLE" = "1" ]]; then
|
||||||
|
# cat >/etc/monit/monitrc.d/55-opendmarc.conf <<'EOF'
|
||||||
|
#check process opendmarc with pidfile /run/opendmarc/opendmarc.pid
|
||||||
|
# start program = "/bin/systemctl start opendmarc"
|
||||||
|
# stop program = "/bin/systemctl stop opendmarc"
|
||||||
|
# if 5 restarts within 5 cycles then alert
|
||||||
|
#EOF
|
||||||
|
#else
|
||||||
|
# rm -f /etc/monit/monitrc.d/55-opendmarc.conf || true
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
## 60 – ClamAV (über Socket)
|
||||||
|
#if [[ "$CLAMAV_ENABLE" = "1" ]]; then
|
||||||
|
# cat >/etc/monit/monitrc.d/60-clamav.conf <<'EOF'
|
||||||
|
#check process clamd matching "clamd"
|
||||||
|
# start program = "/bin/systemctl start clamav-daemon"
|
||||||
|
# stop program = "/bin/systemctl stop clamav-daemon"
|
||||||
|
# if failed unixsocket /run/clamav/clamd.ctl for 3 cycles then restart
|
||||||
|
# if 5 restarts within 10 cycles then unmonitor
|
||||||
|
#EOF
|
||||||
|
#else
|
||||||
|
# rm -f /etc/monit/monitrc.d/60-clamav.conf || true
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
## 70 – Fail2Ban (optional)
|
||||||
|
#if [[ "$FAIL2BAN_ENABLE" = "1" ]]; then
|
||||||
|
# cat >/etc/monit/monitrc.d/70-fail2ban.conf <<'EOF'
|
||||||
|
#check process fail2ban with pidfile /run/fail2ban/fail2ban.pid
|
||||||
|
# start program = "/bin/systemctl start fail2ban"
|
||||||
|
# stop program = "/bin/systemctl stop fail2ban"
|
||||||
|
# if 5 restarts within 5 cycles then alert
|
||||||
|
#EOF
|
||||||
|
#else
|
||||||
|
# rm -f /etc/monit/monitrc.d/70-fail2ban.conf || true
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
## ── Monit neu laden ─────────────────────────────────────────────────────────
|
||||||
|
#monit -t
|
||||||
|
#systemctl reload monit || systemctl restart monit
|
||||||
|
#
|
||||||
|
## Optionaler Sichttest (CLI funktioniert auch ohne HTTP-UI)
|
||||||
|
##sleep 2
|
||||||
|
##monit summary || true
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
#!/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
|
||||||
|
# MOTD – MailWolt
|
||||||
|
# bewusst KEIN "set -e" und KEIN pipefail; MOTD darf nie hart abbrechen
|
||||||
|
set -u
|
||||||
|
|
||||||
|
# ---------- Farben ----------
|
||||||
|
NC="\033[0m"; WH="\033[1;37m"; CY="\033[1;36m"; GY="\033[0;90m"
|
||||||
|
GR="\033[1;32m"; YE="\033[1;33m"; RD="\033[1;31m"
|
||||||
|
|
||||||
|
# ---------- Breite / Zentrierung ----------
|
||||||
|
W=110
|
||||||
|
term_cols=$(tput cols 2>/dev/null || echo $W)
|
||||||
|
[ "$term_cols" -gt "$W" ] && pad=$(( (term_cols - W)/2 )) || pad=0
|
||||||
|
sp(){ [ "$1" -gt 0 ] && printf "%${1}s" " " || true; }
|
||||||
|
center() { local s="$1"; local n=$(( (W - ${#s})/2 )); sp $((pad+n)); printf "%s\n" "$s"; }
|
||||||
|
rule(){ sp "$pad"; printf "%0.s=" $(seq 1 "$W"); printf "\n"; }
|
||||||
|
title(){ sp "$pad"; local t="$1"; local lf=$(( (W - ${#t} - 2)/2 )); local rf=$(( W - ${#t} - 2 - lf )); \
|
||||||
|
printf "%s" "$(printf '─%.0s' $(seq 1 $lf))"; printf " %s " "$t"; printf "%s\n" "$(printf '─%.0s' $(seq 1 $rf))"; }
|
||||||
|
kv(){ sp "$pad"; printf "%-12s: %s\n" "$1" "$2"; }
|
||||||
|
|
||||||
|
# ---------- Installer-/App-Variablen ----------
|
||||||
|
UI_HOST=""; WEBMAIL_HOST=""; MAIL_HOSTNAME=""
|
||||||
|
[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env || true
|
||||||
|
|
||||||
|
# ---------- Systemdaten ----------
|
||||||
|
now="$(date '+%Y-%m-%d %H:%M:%S %Z' 2>/dev/null || echo '-')"
|
||||||
|
upt="$(uptime -p 2>/dev/null || echo '-')"
|
||||||
|
cores="$(nproc 2>/dev/null || echo 1)"
|
||||||
|
load_raw="$(awk '{printf "%s / %s / %s",$1,$2,$3}' /proc/loadavg 2>/dev/null || echo '0.00 / 0.00 / 0.00')"
|
||||||
|
load1="$(awk '{print $1}' /proc/loadavg 2>/dev/null || echo 0)"
|
||||||
|
|
||||||
|
# RAM/SWAP
|
||||||
|
mem_total="$(awk '/MemTotal/ {print int($2/1024)}' /proc/meminfo 2>/dev/null || echo 0)"
|
||||||
|
mem_avail="$(awk '/MemAvailable/ {print int($2/1024)}' /proc/meminfo 2>/dev/null || echo 0)"
|
||||||
|
mem_used=$(( mem_total - mem_avail ))
|
||||||
|
swap_total="$(awk '/SwapTotal/ {print int($2/1024)}' /proc/meminfo 2>/dev/null || echo 0)"
|
||||||
|
swap_free="$(awk '/SwapFree/ {print int($2/1024)}' /proc/meminfo 2>/dev/null || echo 0)"
|
||||||
|
swap_used=$(( swap_total - swap_free ))
|
||||||
|
|
||||||
|
pct(){ local u="$1" t="$2"; [ "$t" -gt 0 ] || { echo 0; return; }; awk -v u="$u" -v t="$t" 'BEGIN{printf "%d",(u*100)/t}' ; }
|
||||||
|
ram_pct=$(pct "$mem_used" "$mem_total")
|
||||||
|
swap_pct=$(pct "$swap_used" "$swap_total")
|
||||||
|
|
||||||
|
# Disks
|
||||||
|
df_line(){ df -hP "$1" 2>/dev/null | awk 'NR==2{printf "%s / %s (%s)",$3,$2,$5}'; }
|
||||||
|
df_pct(){ df -P "$1" 2>/dev/null | awk 'NR==2{gsub("%","",$5);print $5+0}'; }
|
||||||
|
disk_root="$(df_line /)"; pct_root="$(df_pct /)"
|
||||||
|
disk_var="$(df_line /var 2>/dev/null)"; [ -n "$disk_var" ] || disk_var="-"
|
||||||
|
pct_var="$(df_pct /var 2>/dev/null)"; [ -n "$pct_var" ] || pct_var=0
|
||||||
|
|
||||||
|
# IPs (int/ext)
|
||||||
|
ipv4_int="$(hostname -I 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i!~/:/){print $i;exit}}')"
|
||||||
|
ipv6_int="$(hostname -I 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i~/:/){print $i;exit}}')"
|
||||||
|
ipv4_ext="$(curl -4fsS --max-time 1 https://ifconfig.me 2>/dev/null || true)"
|
||||||
|
ipv6_ext="$(curl -6fsS --max-time 1 https://ifconfig.me 2>/dev/null || true)"
|
||||||
|
|
||||||
|
# ---------- Status-Farben ----------
|
||||||
|
mark(){ # value thresholdY thresholdR
|
||||||
|
local v="$1" y="$2" r="$3"
|
||||||
|
if [ "$v" -ge "$r" ]; then printf "${RD}[HIGH]${NC}"
|
||||||
|
elif [ "$v" -ge "$y" ]; then printf "${YE}[WARN]${NC}"
|
||||||
|
else printf "${GR}[OK]${NC}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
# Load/CPU-Schwellen (pro Core)
|
||||||
|
load_pct=$(awk -v l="$load1" -v c="$cores" 'BEGIN{if(c<1)c=1; printf "%d", (l/c)*100}')
|
||||||
|
m_load="$(mark "$load_pct" 70 100)"
|
||||||
|
m_ram="$(mark "$ram_pct" 75 90)"
|
||||||
|
m_swap="$(mark "$swap_pct" 10 50)"
|
||||||
|
m_root="$(mark "$pct_root" 75 90)"
|
||||||
|
m_var="$(mark "$pct_var" 75 90)"
|
||||||
|
|
||||||
|
# ---------- Header ----------
|
||||||
|
rule
|
||||||
|
center ""
|
||||||
|
center ":::: :::: ::: ::::::::::: ::: ::: ::: :::::::: ::: :::::::::::"
|
||||||
|
center ":+:+:+ :+:+:+ :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: "
|
||||||
|
center ":+: +:+:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ "
|
||||||
|
center "+#+ +:+ +#+ +#++:++#++: +#+ +#+ +#+ +:+ +#+ +#+ +:+ +#+ +#+ "
|
||||||
|
center "+#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+#+ +#+ +#+ +#+ +#+ +#+ "
|
||||||
|
center "#+# #+# #+# #+# #+# #+# #+#+# #+#+# #+# #+# #+# #+# "
|
||||||
|
center "### ### ### ### ########### ########## ### ### ######## ########## ### "
|
||||||
|
center ""
|
||||||
|
rule
|
||||||
|
|
||||||
|
# ---------- System ----------
|
||||||
|
kv "Date / Time" "${YE}${now}${NC}"
|
||||||
|
sp "$pad"; printf "%-12s: int %-40s ext %s\n" "IPv4" "${ipv4_int:--}" "${ipv4_ext:--}"
|
||||||
|
sp "$pad"; printf "%-12s: int %-40s ext %s\n" "IPv6" "${ipv6_int:--}" "${ipv6_ext:--}"
|
||||||
|
kv "Uptime" "$upt"
|
||||||
|
sp "$pad"; printf "%-12s: %s cores, load %s %b\n" "CPU" "$cores" "$load_raw" "$m_load"
|
||||||
|
sp "$pad"; printf "%-12s: %s MiB / %s MiB (%d%%) %b %-5s %s MiB / %s MiB (%d%%) %b\n" \
|
||||||
|
"RAM" "$mem_used" "$mem_total" "$ram_pct" "$m_ram" "SWAP:" "$swap_used" "$swap_total" "$swap_pct" "$m_swap"
|
||||||
|
sp "$pad"; printf "%-12s: / %s %b %-5s %s %b\n" \
|
||||||
|
"Disk" "$disk_root" "$m_root" "/var:" "$disk_var" "$m_var"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# ---------- Domains ----------
|
||||||
|
title "Domains"
|
||||||
|
[ -n "${UI_HOST:-}" ] && kv "UI" "${UI_HOST}"
|
||||||
|
[ -n "${WEBMAIL_HOST:-}" ] && kv "Webmail" "${WEBMAIL_HOST}"
|
||||||
|
[ -n "${MAIL_HOSTNAME:-}" ]&& kv "MX" "${MAIL_HOSTNAME}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# ---------- Services (4 Spalten, bündig) ----------
|
||||||
|
title "Services"
|
||||||
|
svc_state(){ systemctl is-active --quiet "$1" && printf "${GR}[OK]${NC}" || printf "${RD}[FAIL]${NC}"; }
|
||||||
|
SVC=( nginx mariadb redis-server postfix dovecot rspamd opendkim opendmarc clamav-daemon fail2ban mailwolt-ws mailwolt-queue mailwolt-schedule )
|
||||||
|
|
||||||
|
i=0; line=""
|
||||||
|
for s in "${SVC[@]}"; do
|
||||||
|
st="$(svc_state "$s")"
|
||||||
|
seg="$(printf "%-18s %-7s" "$s" "$st")"
|
||||||
|
line="$line$seg"
|
||||||
|
i=$((i+1))
|
||||||
|
if [ $((i%4)) -eq 0 ]; then sp "$pad"; echo "$line"; line=""; else line="$line "; fi
|
||||||
|
done
|
||||||
|
[ -n "$line" ] && { sp "$pad"; echo "$line"; }
|
||||||
|
echo
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
SH
|
||||||
|
|
||||||
|
chmod 755 /usr/local/bin/mw-motd
|
||||||
|
|
||||||
|
# update-motd Hook
|
||||||
|
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
|
||||||
|
# Fallback für Systeme ohne dynamic MOTD
|
||||||
|
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
|
||||||
|
log "[✓] MOTD installiert."
|
||||||
|
|
@ -0,0 +1,212 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# MailWolt – Abschluss / Summary (Dienste, Zertifikate, Smoke-Test)
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Farben & Deko
|
||||||
|
NC="\033[0m"; BOLD="\033[1m"; DIM="\033[2m"
|
||||||
|
GREEN="\033[1;32m"; RED="\033[1;31m"; YELLOW="\033[1;33m"; CYAN="\033[1;36m"; GREY="\033[0;90m"
|
||||||
|
OKS="${GREEN}OK${NC}"; FAILS="${RED}FAIL${NC}"
|
||||||
|
bar(){ printf "${CYAN}%s${NC}\n" "──────────────────────────────────────────────────────────────────────────────"; }
|
||||||
|
ok(){ printf " [${OKS}]\n"; }
|
||||||
|
fail(){ printf " [${FAILS}]\n"; }
|
||||||
|
|
||||||
|
# Installer-Variablen laden (falls vorhanden)
|
||||||
|
set +u
|
||||||
|
[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
|
||||||
|
set -u
|
||||||
|
|
||||||
|
# Defaults / Umgebung
|
||||||
|
APP_USER="${APP_USER:-mailwolt}"
|
||||||
|
APP_GROUP="${APP_GROUP:-www-data}"
|
||||||
|
APP_DIR="${APP_DIR:-/var/www/${APP_USER}}"
|
||||||
|
|
||||||
|
BASE_DOMAIN="${BASE_DOMAIN:-example.com}"
|
||||||
|
UI_HOST="${UI_HOST:-}"
|
||||||
|
WEBMAIL_HOST="${WEBMAIL_HOST:-}"
|
||||||
|
MAIL_HOSTNAME="${MAIL_HOSTNAME:-}"
|
||||||
|
|
||||||
|
APP_ENV="${APP_ENV:-production}"
|
||||||
|
PROXY_MODE="${PROXY_MODE:-}" # leer = nicht anzeigen; "1"=Proxy, "dev"=Dev, sonst "nein"
|
||||||
|
NPM_IP="${NPM_IP:-}"
|
||||||
|
|
||||||
|
LE_EMAIL="${LE_EMAIL:-admin@${BASE_DOMAIN}}"
|
||||||
|
ACME_WEBROOT="/var/www/letsencrypt"
|
||||||
|
|
||||||
|
# Zert-Pfade (werden via Hook nach /etc/ssl/* verlinkt)
|
||||||
|
UI_CERT="/etc/ssl/ui/fullchain.pem"
|
||||||
|
UI_KEY="/etc/ssl/ui/privkey.pem"
|
||||||
|
WEBMAIL_CERT="/etc/ssl/webmail/fullchain.pem"
|
||||||
|
MAIL_CERT="/etc/ssl/mail/fullchain.pem"
|
||||||
|
|
||||||
|
# IPs (aus lib.sh)
|
||||||
|
SERVER_PUBLIC_IPV4="${SERVER_PUBLIC_IPV4:-$(detect_ip)}"
|
||||||
|
SERVER_PUBLIC_IPV6="${SERVER_PUBLIC_IPV6:-$(detect_ipv6)}"
|
||||||
|
|
||||||
|
# URLs (https nur, wenn UI-Cert+Key vorhanden)
|
||||||
|
SCHEME="http"
|
||||||
|
[[ -s "$UI_CERT" && -s "$UI_KEY" ]] && SCHEME="https"
|
||||||
|
APP_URL="${SCHEME}://${UI_HOST:-$SERVER_PUBLIC_IPV4}"
|
||||||
|
WEBMAIL_URL="${SCHEME}://${WEBMAIL_HOST:-$SERVER_PUBLIC_IPV4}"
|
||||||
|
|
||||||
|
# Ziel eines Symlinks auflösen
|
||||||
|
real_target(){ readlink -f -- "$1" 2>/dev/null || true; }
|
||||||
|
|
||||||
|
# "LE" werten, wenn live/* ODER archive/* (auch fullchainN.pem) getroffen wird
|
||||||
|
is_le_path(){
|
||||||
|
local p="$1"
|
||||||
|
[[ "$p" == /etc/letsencrypt/live/*/fullchain.pem || "$p" == /etc/letsencrypt/archive/*/fullchain*.pem ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
UI_CERT_TARGET="$(real_target "$UI_CERT")"
|
||||||
|
WEBMAIL_CERT_TARGET="$(real_target "$WEBMAIL_CERT")"
|
||||||
|
MAIL_CERT_TARGET="$(real_target "$MAIL_CERT")"
|
||||||
|
|
||||||
|
is_le_path() {
|
||||||
|
case "$1" in
|
||||||
|
/etc/letsencrypt/live/*) return 0 ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# robust gegen set -u: immer ${var:-}
|
||||||
|
UI_LE="self-signed/none"
|
||||||
|
if [ -s "${UI_CERT:-}" ] && [ -n "${UI_CERT_TARGET:-}" ] && is_le_path "${UI_CERT_TARGET:-}"; then
|
||||||
|
UI_LE="LE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
WEBMAIL_LE="self-signed/none"
|
||||||
|
if [ -s "${WEBMAIL_CERT:-}" ] && [ -n "${WEBMAIL_CERT_TARGET:-}" ] && is_le_path "${WEBMAIL_CERT_TARGET:-}"; then
|
||||||
|
WEBMAIL_LE="LE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
MAIL_LE="self-signed/none"
|
||||||
|
if [ -s "${MAIL_CERT:-}" ] && [ -n "${MAIL_CERT_TARGET:-}" ] && is_le_path "${MAIL_CERT_TARGET:-}"; then
|
||||||
|
MAIL_LE="LE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
bar
|
||||||
|
printf " %s\n" "✔ MailWolt Bootstrap fertig"
|
||||||
|
bar
|
||||||
|
|
||||||
|
# Kopf-Infos
|
||||||
|
printf " %-14s %s\n" "Aufruf UI:" "${APP_URL}"
|
||||||
|
printf " %-14s %s\n" "Webmail:" "${WEBMAIL_URL}"
|
||||||
|
printf " %-14s %s\n" "App Root:" "${APP_DIR}"
|
||||||
|
printf " %-14s %s\n" "Mail-FQDN:" "${MAIL_HOSTNAME:-$SERVER_PUBLIC_IPV4}"
|
||||||
|
printf " %-14s %s\n" "BASE_DOMAIN:" "${BASE_DOMAIN}"
|
||||||
|
printf " %-14s %s\n" "LE-Email:" "${LE_EMAIL}"
|
||||||
|
printf " %-14s %s\n" "APP_ENV:" "${APP_ENV}"
|
||||||
|
# Proxy-Block nur anzeigen, wenn Variable gesetzt ist
|
||||||
|
if [[ -n "$PROXY_MODE" ]]; then
|
||||||
|
if [[ "$PROXY_MODE" == "1" ]]; then
|
||||||
|
printf " %-14s %s\n" "Proxy-Mode:" "ja (NPM: ${NPM_IP:-unbekannt})"
|
||||||
|
elif [[ "$PROXY_MODE" == "dev" ]]; then
|
||||||
|
printf " %-14s %s\n" "Proxy-Mode:" "Entwicklungsmodus"
|
||||||
|
else
|
||||||
|
printf " %-14s %s\n" "Proxy-Mode:" "nein"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
printf " %-14s %s\n" "Server IPv6:" "${SERVER_PUBLIC_IPV6:-–}"
|
||||||
|
printf " %-14s %s\n" "ACME Webroot:" "${ACME_WEBROOT}"
|
||||||
|
|
||||||
|
echo
|
||||||
|
printf " %-14s UI=%s, Webmail=%s, MX=%s\n" "Zertifikate:" "$UI_LE" "$WEBMAIL_LE" "$MAIL_LE"
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo " Anmeldung: Keine vordefinierten Admin-Daten."
|
||||||
|
echo " Bitte zuerst registrieren (Erst-User wird Admin, danach"
|
||||||
|
echo " wird die Registrierung automatisch gesperrt)."
|
||||||
|
echo
|
||||||
|
|
||||||
|
# ── Dienste ────────────────────────────────────────────────────────────────
|
||||||
|
bar
|
||||||
|
echo " Services"
|
||||||
|
bar
|
||||||
|
|
||||||
|
OK_LIST=()
|
||||||
|
FAIL_LIST=()
|
||||||
|
|
||||||
|
svc(){
|
||||||
|
local unit="$1" label="${2:-$1}"
|
||||||
|
printf " • %-18s … " "$label"
|
||||||
|
if systemctl is-active --quiet "$unit"; then
|
||||||
|
ok
|
||||||
|
OK_LIST+=("$label")
|
||||||
|
else
|
||||||
|
fail
|
||||||
|
FAIL_LIST+=("$label")
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Kern-Services
|
||||||
|
svc nginx
|
||||||
|
svc mariadb
|
||||||
|
svc redis-server
|
||||||
|
svc postfix
|
||||||
|
svc dovecot
|
||||||
|
# App-Worker (tolerant)
|
||||||
|
svc "${APP_USER}-ws" "mailwolt-ws" || true
|
||||||
|
svc "${APP_USER}-schedule" "mailwolt-schedule" || true
|
||||||
|
svc "${APP_USER}-queue" "mailwolt-queue" || true
|
||||||
|
|
||||||
|
echo
|
||||||
|
if ((${#OK_LIST[@]})); then
|
||||||
|
printf " ${GREEN}OK:${NC} %s\n" "$(IFS=', '; echo "${OK_LIST[*]}")"
|
||||||
|
fi
|
||||||
|
if ((${#FAIL_LIST[@]})); then
|
||||||
|
printf " ${RED}FAIL:${NC} %s\n" "$(IFS=', '; echo "${FAIL_LIST[*]}")"
|
||||||
|
echo " ${YELLOW}Hinweis:${NC} Details mit: journalctl -u <dienst> -b --no-pager"
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
|
||||||
|
# ── Smoke-Test ─────────────────────────────────────────────────────────────
|
||||||
|
bar
|
||||||
|
echo " Smoke-Test (SMTP/IMAP/POP3 mit/ohne TLS)"
|
||||||
|
bar
|
||||||
|
|
||||||
|
check_port(){
|
||||||
|
local tag="$1" cmd="$2" desc="$3"
|
||||||
|
printf " [%-3s] %-35s … " "$tag" "$desc"
|
||||||
|
if timeout 8s bash -lc "$cmd" >/dev/null 2>&1; then ok; else fail; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# kleines Delay nach Erststart
|
||||||
|
sleep 6 || true
|
||||||
|
|
||||||
|
# SMTP
|
||||||
|
check_port "25" 'printf "EHLO x\r\nQUIT\r\n" | nc -w 3 127.0.0.1 25' \
|
||||||
|
"SMTP (EHLO)"
|
||||||
|
check_port "465" 'printf "QUIT\r\n" | openssl s_client -connect 127.0.0.1:465 -quiet -ign_eof' \
|
||||||
|
"SMTPS (TLS + EHLO)"
|
||||||
|
check_port "587" 'printf "EHLO x\r\nSTARTTLS\r\nQUIT\r\n" | openssl s_client -starttls smtp -connect 127.0.0.1:587 -quiet -ign_eof' \
|
||||||
|
"Submission (STARTTLS)"
|
||||||
|
|
||||||
|
# POP/IMAP
|
||||||
|
check_port "110" 'printf "QUIT\r\n" | nc -w 3 127.0.0.1 110' \
|
||||||
|
"POP3 (QUIT)"
|
||||||
|
check_port "995" 'printf "QUIT\r\n" | openssl s_client -connect 127.0.0.1:995 -quiet -ign_eof' \
|
||||||
|
"POP3S (TLS + QUIT)"
|
||||||
|
check_port "143" 'printf ". CAPABILITY\r\n. LOGOUT\r\n" | nc -w 3 127.0.0.1 143' \
|
||||||
|
"IMAP (CAPABILITY/LOGOUT)"
|
||||||
|
check_port "993" 'printf ". CAPABILITY\r\n. LOGOUT\r\n" | openssl s_client -connect 127.0.0.1:993 -quiet -ign_eof' \
|
||||||
|
"IMAPS (TLS + CAPABILITY/LOGOUT)"
|
||||||
|
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Hinweise nur ausgeben, wenn wirklich kein LE für UI/Webmail
|
||||||
|
if [[ "$UI_LE" != "LE" || "$WEBMAIL_LE" != "LE" ]]; then
|
||||||
|
echo -e " ${YELLOW}Hinweis:${NC} UI/Webmail verwenden noch kein Let's-Encrypt-Zertifikat."
|
||||||
|
echo -e " Prüfe Symlinks unter /etc/ssl/{ui,webmail} und den LE-Hook (21/75-Skripte)."
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Proxy-Info (optional)
|
||||||
|
if [[ "$PROXY_MODE" == "1" ]]; then
|
||||||
|
echo -e " ${GREY}Proxy-Hinweis:${NC} App erwartet TLS am Proxy (Backend ohne https-Redirects)."
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
@ -0,0 +1,633 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# --- Farbschema für whiptail (libnewt) – hohe Lesbarkeit (dunkler Input, schwarze Schrift) ---
|
||||||
|
export NEWT_COLORS='
|
||||||
|
root=,blue
|
||||||
|
border=black,lightgray
|
||||||
|
window=black,lightgray
|
||||||
|
textbox=black,lightgray
|
||||||
|
label=black,lightgray
|
||||||
|
entry=black,cyan
|
||||||
|
button=black,cyan
|
||||||
|
actlistbox=black,cyan
|
||||||
|
actsellistbox=black,cyan
|
||||||
|
'
|
||||||
|
|
||||||
|
# optionales Backtitle (erscheint oben)
|
||||||
|
export DIALOGOPTS="--backtitle MailWolt Setup"
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────
|
||||||
|
# MailWolt – Interaktiver Bootstrap (whiptail + Fallback)
|
||||||
|
# ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
DEV_MODE=0
|
||||||
|
PROXY_MODE=0
|
||||||
|
NPM_IP=""
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-dev) DEV_MODE=1 ;;
|
||||||
|
-proxy) PROXY_MODE=1; NPM_IP="${2:-}"; shift ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
APP_ENV="${APP_ENV:-$([[ $DEV_MODE -eq 1 ]] && echo local || echo production)}"
|
||||||
|
APP_DEBUG="${APP_DEBUG:-$([[ $DEV_MODE -eq 1 ]] && echo true || echo false)}"
|
||||||
|
export DEV_MODE PROXY_MODE NPM_IP APP_ENV APP_DEBUG
|
||||||
|
|
||||||
|
DB_PASS="${DB_PASS:-$(openssl rand -hex 16)}"
|
||||||
|
REDIS_PASS="${REDIS_PASS:-$(openssl rand -hex 16)}"
|
||||||
|
export DB_PASS REDIS_PASS
|
||||||
|
|
||||||
|
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}"
|
||||||
|
|
||||||
|
DB_NAME="${DB_NAME:-${APP_USER}}"
|
||||||
|
DB_USER="${DB_USER:-${APP_USER}}"
|
||||||
|
|
||||||
|
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}"
|
||||||
|
|
||||||
|
# ── Helpers ───────────────────────────────────────────────────
|
||||||
|
have_whiptail(){ command -v whiptail >/dev/null 2>&1; }
|
||||||
|
|
||||||
|
#valid_fqdn(){
|
||||||
|
# [[ "$1" =~ ^([a-z0-9]([-a-z0-9]*[a-z0-9])?\.)+[a-z]{2,}$ ]]
|
||||||
|
#}
|
||||||
|
|
||||||
|
# ── Host-Validierung & DEV-Erkennung ────────────────────────────────────────
|
||||||
|
valid_fqdn_prod(){ [[ "$1" =~ ^([a-z0-9]([-a-z0-9]*[a-z0-9])?\.)+[a-z]{2,}$ ]]; }
|
||||||
|
valid_host_dev(){
|
||||||
|
# erlaubt: single-label (ui, webmail), FQDNs, IPv4
|
||||||
|
[[ "$1" =~ ^([a-z0-9]([-a-z0-9]*[a-z0-9])?)(\.[a-z0-9-]+)*$ ]] || [[ "$1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]
|
||||||
|
}
|
||||||
|
is_local_like(){
|
||||||
|
local h="$(echo "$1" | tr '[:upper:]' '[:lower:]')"
|
||||||
|
[[ "$h" =~ \.local$ || "$h" =~ \.loc$ || "$h" =~ \.dev$ || "$h" =~ \.test$ || "$h" = "localhost" ]] && return 0
|
||||||
|
[[ "$h" =~ ^10\. || "$h" =~ ^192\.168\. || "$h" =~ ^172\.(1[6-9]|2[0-9]|3[0-1])\. || "$h" =~ ^127\. ]] && return 0
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
normalize_host(){
|
||||||
|
# $1=input $2=default (nutzt DEV_MODE für die passende Prüflogik)
|
||||||
|
local inp="$1" def="$2"
|
||||||
|
if [[ "${DEV_MODE}" = "1" ]]; then
|
||||||
|
valid_host_dev "$inp" && { echo "$inp"; return; }
|
||||||
|
else
|
||||||
|
valid_fqdn_prod "$inp" && { echo "$inp"; return; }
|
||||||
|
fi
|
||||||
|
echo "$def"
|
||||||
|
}
|
||||||
|
|
||||||
|
ask_tty_domain(){
|
||||||
|
local label="$1" example="$2" def="$3" outvar="$4" inp
|
||||||
|
echo -e "${CYAN}${label}${NC}"
|
||||||
|
echo -e " z.B. ${YELLOW}${example}${NC}"
|
||||||
|
echo -e " Default: ${GREY}${def}${NC}"
|
||||||
|
read -r -p " Eingabe (Enter=Default): " inp || true
|
||||||
|
inp="${inp:-$def}"
|
||||||
|
if ! valid_fqdn "$inp"; then
|
||||||
|
echo -e "${YELLOW}[!] Ungültiger FQDN, nehme Default: ${def}${NC}"
|
||||||
|
inp="$def"
|
||||||
|
fi
|
||||||
|
eval "$outvar='$inp'"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Interaktive Eingaben (whiptail oder Fallback) ─────────────
|
||||||
|
MTA_DEFAULT="${MTA_SUB}.${BASE_DOMAIN}"
|
||||||
|
UI_DEFAULT="${UI_SUB}.${BASE_DOMAIN}"
|
||||||
|
WEBMAIL_DEFAULT="${WEBMAIL_SUB}.${BASE_DOMAIN}"
|
||||||
|
|
||||||
|
CLAMAV_ENABLE=1
|
||||||
|
OPENDMARC_ENABLE=1
|
||||||
|
FAIL2BAN_ENABLE=1
|
||||||
|
|
||||||
|
if command -v whiptail >/dev/null 2>&1; then
|
||||||
|
TITLE="MailWolt Setup"
|
||||||
|
|
||||||
|
# Hinweise zu erlaubten DEV-Hosts
|
||||||
|
MSG_SUFFIX="\n\nHinweis: Im DEV-Modus sind auch single-label Hosts (z.B. ui, webmail), *.local/*.dev und IPs erlaubt."
|
||||||
|
|
||||||
|
_mta_in="$(whiptail --title "$TITLE" --inputbox "Mailserver-Host (MX)\nBeispiele: mx.domain.tld | mx.local | 10.0.0.10${MSG_SUFFIX}" 13 70 "$MTA_DEFAULT" 3>&1 1>&2 2>&3)" || exit 1
|
||||||
|
_ui_in="$(whiptail --title "$TITLE" --inputbox "UI / Admin-Panel Host\nBeispiele: ui.domain.tld | ui.local | 10.0.0.10${MSG_SUFFIX}" 13 70 "$UI_DEFAULT" 3>&1 1>&2 2>&3)" || exit 1
|
||||||
|
_wm_in="$(whiptail --title "$TITLE" --inputbox "Webmail Host\nBeispiele: webmail.domain.tld | web.local | 10.0.0.10${MSG_SUFFIX}" 13 70 "$WEBMAIL_DEFAULT" 3>&1 1>&2 2>&3)" || exit 1
|
||||||
|
|
||||||
|
# ZUERST provisorisch prüfen, ob „lokal“ → DEV erzwingen
|
||||||
|
if is_local_like "$_mta_in" || is_local_like "$_ui_in" || is_local_like "$_wm_in"; then
|
||||||
|
DEV_MODE=1; APP_ENV="local"; APP_DEBUG="true"
|
||||||
|
fi
|
||||||
|
export DEV_MODE APP_ENV APP_DEBUG
|
||||||
|
|
||||||
|
# Jetzt mit passender Logik normalisieren
|
||||||
|
MTA_FQDN="$(normalize_host "$_mta_in" "$MTA_DEFAULT")"
|
||||||
|
UI_FQDN="$(normalize_host "$_ui_in" "$UI_DEFAULT")"
|
||||||
|
WEBMAIL_FQDN="$(normalize_host "$_wm_in" "$WEBMAIL_DEFAULT")"
|
||||||
|
|
||||||
|
CHOICES="$(whiptail --title "$TITLE" --checklist "Optionale Dienste aktivieren" 15 70 6 \
|
||||||
|
"ClamAV" "Virenscan (clamd/clamav-daemon)" ON \
|
||||||
|
"OpenDMARC" "DMARC-Auswertung" ON \
|
||||||
|
"Fail2Ban" "Brute-Force-Schutz" ON \
|
||||||
|
3>&1 1>&2 2>&3)" || true
|
||||||
|
CLAMAV_ENABLE=0; [[ "$CHOICES" == *"ClamAV"* ]] && CLAMAV_ENABLE=1
|
||||||
|
OPENDMARC_ENABLE=0; [[ "$CHOICES" == *"OpenDMARC"* ]] && OPENDMARC_ENABLE=1
|
||||||
|
FAIL2BAN_ENABLE=0; [[ "$CHOICES" == *"Fail2Ban"* ]] && FAIL2BAN_ENABLE=1
|
||||||
|
|
||||||
|
else
|
||||||
|
echo -e "${GREY}[i] whiptail nicht gefunden – TTY-Fallback.${NC}\n"
|
||||||
|
read -r -p "Mailserver-Host (MX) [${MTA_DEFAULT}]: " _mta_in; _mta_in="${_mta_in:-$MTA_DEFAULT}"
|
||||||
|
read -r -p "UI / Admin-Panel Host [${UI_DEFAULT}]: " _ui_in; _ui_in="${_ui_in:-$UI_DEFAULT}"
|
||||||
|
read -r -p "Webmail Host [${WEBMAIL_DEFAULT}]: " _wm_in; _wm_in="${_wm_in:-$WEBMAIL_DEFAULT}"
|
||||||
|
|
||||||
|
if is_local_like "$_mta_in" || is_local_like "$_ui_in" || is_local_like "$_wm_in"; then
|
||||||
|
DEV_MODE=1; APP_ENV="local"; APP_DEBUG="true"
|
||||||
|
fi
|
||||||
|
export DEV_MODE APP_ENV APP_DEBUG
|
||||||
|
|
||||||
|
MTA_FQDN="$(normalize_host "$_mta_in" "$MTA_DEFAULT")"
|
||||||
|
UI_FQDN="$(normalize_host "$_ui_in" "$UI_DEFAULT")"
|
||||||
|
WEBMAIL_FQDN="$(normalize_host "$_wm_in" "$WEBMAIL_DEFAULT")"
|
||||||
|
|
||||||
|
read -r -p "ClamAV aktivieren? (1/0, Enter=1): " CLAMAV_ENABLE; CLAMAV_ENABLE="${CLAMAV_ENABLE:-1}"
|
||||||
|
read -r -p "OpenDMARC aktivieren? (1/0, Enter=1): " OPENDMARC_ENABLE; OPENDMARC_ENABLE="${OPENDMARC_ENABLE:-1}"
|
||||||
|
read -r -p "Fail2Ban aktivieren? (1/0, Enter=1): " FAIL2BAN_ENABLE; FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
#if have_whiptail; then
|
||||||
|
# TITLE="MailWolt Setup"
|
||||||
|
#
|
||||||
|
# MTA_FQDN="$(whiptail --title "$TITLE" --inputbox "Mailserver-FQDN (MX)\nBeispiel: mx.domain.tld" 11 70 "$MTA_DEFAULT" 3>&1 1>&2 2>&3)" || exit 1
|
||||||
|
# valid_fqdn "$MTA_FQDN" || MTA_FQDN="$MTA_DEFAULT"
|
||||||
|
#
|
||||||
|
# UI_FQDN="$(whiptail --title "$TITLE" --inputbox "UI / Admin-Panel FQDN\nBeispiel: ui.domain.tld" 11 70 "$UI_DEFAULT" 3>&1 1>&2 2>&3)" || exit 1
|
||||||
|
# valid_fqdn "$UI_FQDN" || UI_FQDN="$UI_DEFAULT"
|
||||||
|
#
|
||||||
|
# WEBMAIL_FQDN="$(whiptail --title "$TITLE" --inputbox "Webmail FQDN\nBeispiel: webmail.domain.tld" 11 70 "$WEBMAIL_DEFAULT" 3>&1 1>&2 2>&3)" || exit 1
|
||||||
|
# valid_fqdn "$WEBMAIL_FQDN" || WEBMAIL_FQDN="$WEBMAIL_DEFAULT"
|
||||||
|
#
|
||||||
|
# CHOICES="$(whiptail --title "$TITLE" --checklist "Optionale Dienste aktivieren" 15 70 6 \
|
||||||
|
# "ClamAV" "Virenscan (clamd/clamav-daemon)" ON \
|
||||||
|
# "OpenDMARC" "DMARC-Auswertung" ON \
|
||||||
|
# "Fail2Ban" "Brute-Force-Schutz" ON \
|
||||||
|
# 3>&1 1>&2 2>&3)" || true
|
||||||
|
#
|
||||||
|
# CLAMAV_ENABLE=0; [[ "$CHOICES" == *"ClamAV"* ]] && CLAMAV_ENABLE=1
|
||||||
|
# OPENDMARC_ENABLE=0; [[ "$CHOICES" == *"OpenDMARC"* ]] && OPENDMARC_ENABLE=1
|
||||||
|
# FAIL2BAN_ENABLE=0; [[ "$CHOICES" == *"Fail2Ban"* ]] && FAIL2BAN_ENABLE=1
|
||||||
|
#
|
||||||
|
# whiptail --title "$TITLE" --msgbox "Zusammenfassung:
|
||||||
|
#
|
||||||
|
#MX : $MTA_FQDN
|
||||||
|
#UI : $UI_FQDN
|
||||||
|
#Webmail : $WEBMAIL_FQDN
|
||||||
|
#
|
||||||
|
#ClamAV : $([[ $CLAMAV_ENABLE -eq 1 ]] && echo Aktiv || echo Deaktiv)
|
||||||
|
#OpenDMARC : $([[ $OPENDMARC_ENABLE -eq 1 ]] && echo Aktiv || echo Deaktiv)
|
||||||
|
#Fail2Ban : $([[ $FAIL2BAN_ENABLE -eq 1 ]] && echo Aktiv || echo Deaktiv)
|
||||||
|
#" 16 70
|
||||||
|
#
|
||||||
|
#else
|
||||||
|
# echo -e "${GREY}[i] whiptail nicht gefunden – nutze TTY-Prompts.${NC}\n"
|
||||||
|
# ask_tty_domain "Mailserver-FQDN (MX)" "mx.domain.tld" "$MTA_DEFAULT" MTA_FQDN
|
||||||
|
# ask_tty_domain "UI / Admin-Panel FQDN" "ui.domain.tld" "$UI_DEFAULT" UI_FQDN
|
||||||
|
# ask_tty_domain "Webmail FQDN" "webmail.domain.tld" "$WEBMAIL_DEFAULT" WEBMAIL_FQDN
|
||||||
|
#
|
||||||
|
# read -r -p "ClamAV aktivieren? (1/0, Enter=1): " CLAMAV_ENABLE; CLAMAV_ENABLE="${CLAMAV_ENABLE:-1}"
|
||||||
|
# read -r -p "OpenDMARC aktivieren? (1/0, Enter=1): " OPENDMARC_ENABLE; OPENDMARC_ENABLE="${OPENDMARC_ENABLE:-1}"
|
||||||
|
# read -r -p "Fail2Ban aktivieren? (1/0, Enter=1): " FAIL2BAN_ENABLE; FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}"
|
||||||
|
#fi
|
||||||
|
|
||||||
|
# ── Defaults/Kompatibilität ──────────────────────────────────
|
||||||
|
MTA_FQDN="${MTA_FQDN:-${MTA_DEFAULT}}"
|
||||||
|
UI_FQDN="${UI_FQDN:-${UI_DEFAULT}}"
|
||||||
|
WEBMAIL_FQDN="${WEBMAIL_FQDN:-${WEBMAIL_DEFAULT}}"
|
||||||
|
DKIM_ENABLE="${DKIM_ENABLE:-1}"
|
||||||
|
DKIM_SELECTOR="${DKIM_SELECTOR:-mwl1}"
|
||||||
|
DKIM_GENERATE="${DKIM_GENERATE:-1}"
|
||||||
|
|
||||||
|
# BASE_DOMAIN/Subs aus FQDNs ableiten
|
||||||
|
if [[ "$MTA_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then MTA_SUB="${BASH_REMATCH[1]}"; BASE_DOMAIN="${BASH_REMATCH[2]}"; fi
|
||||||
|
if [[ "$UI_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then UI_SUB="${BASH_REMATCH[1]}"; fi
|
||||||
|
if [[ "$WEBMAIL_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then WEBMAIL_SUB="${BASH_REMATCH[1]}"; fi
|
||||||
|
|
||||||
|
SYSMAIL_SUB="${SYSMAIL_SUB:-sysmail}"
|
||||||
|
SYSMAIL_DOMAIN="${SYSMAIL_SUB}.${BASE_DOMAIN}"
|
||||||
|
|
||||||
|
MAIL_HOSTNAME="${MTA_FQDN}"
|
||||||
|
UI_HOST="${UI_FQDN}"
|
||||||
|
WEBMAIL_HOST="${WEBMAIL_FQDN}"
|
||||||
|
|
||||||
|
APP_TZ="${APP_TZ:-$DEFAULT_TZ}"
|
||||||
|
APP_LOCALE="${APP_LOCALE:-$DEFAULT_LOCALE}"
|
||||||
|
|
||||||
|
# ── Export & persist ─────────────────────────────────────────
|
||||||
|
export APP_NAME APP_USER APP_GROUP APP_USER_PREFIX APP_DIR
|
||||||
|
export BASE_DOMAIN UI_SUB WEBMAIL_SUB MTA_SUB
|
||||||
|
export SYSMAIL_SUB SYSMAIL_DOMAIN DKIM_ENABLE DKIM_SELECTOR DKIM_GENERATE
|
||||||
|
export UI_HOST WEBMAIL_HOST MAIL_HOSTNAME
|
||||||
|
export DB_NAME DB_USER
|
||||||
|
export SERVER_PUBLIC_IPV4 SERVER_PUBLIC_IPV6 APP_TZ APP_LOCALE
|
||||||
|
export CLAMAV_ENABLE OPENDMARC_ENABLE FAIL2BAN_ENABLE
|
||||||
|
|
||||||
|
install -d -m 0755 /etc/mailwolt
|
||||||
|
cat >/etc/mailwolt/installer.env <<EOF
|
||||||
|
SERVER_PUBLIC_IPV4=${SERVER_PUBLIC_IPV4}
|
||||||
|
SERVER_PUBLIC_IPV6=${SERVER_PUBLIC_IPV6}
|
||||||
|
APP_TZ=${APP_TZ}
|
||||||
|
APP_LOCALE=${APP_LOCALE}
|
||||||
|
BASE_DOMAIN=${BASE_DOMAIN}
|
||||||
|
MTA_SUB=${MTA_SUB}
|
||||||
|
UI_SUB=${UI_SUB}
|
||||||
|
WEBMAIL_SUB=${WEBMAIL_SUB}
|
||||||
|
|
||||||
|
MAIL_HOSTNAME=${MAIL_HOSTNAME}
|
||||||
|
UI_HOST=${UI_HOST}
|
||||||
|
WEBMAIL_HOST=${WEBMAIL_HOST}
|
||||||
|
|
||||||
|
LE_EMAIL=${LE_EMAIL:-admin@${BASE_DOMAIN}}
|
||||||
|
SYSMAIL_SUB=${SYSMAIL_SUB}
|
||||||
|
SYSMAIL_DOMAIN=${SYSMAIL_DOMAIN}
|
||||||
|
|
||||||
|
DKIM_ENABLE=${DKIM_ENABLE}
|
||||||
|
DKIM_SELECTOR=${DKIM_SELECTOR}
|
||||||
|
DKIM_GENERATE=${DKIM_GENERATE}
|
||||||
|
|
||||||
|
DB_HOST=127.0.0.1
|
||||||
|
DB_NAME=${DB_NAME}
|
||||||
|
DB_USER=${DB_USER}
|
||||||
|
DB_PASS=${DB_PASS}
|
||||||
|
|
||||||
|
REDIS_HOST=127.0.0.1
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_PASS=${REDIS_PASS}
|
||||||
|
|
||||||
|
SERVER_PUBLIC_IPV4=${SERVER_PUBLIC_IPV4}
|
||||||
|
SERVER_PUBLIC_IPV6=${SERVER_PUBLIC_IPV6}
|
||||||
|
APP_ENV=${APP_ENV}
|
||||||
|
|
||||||
|
CLAMAV_ENABLE=${CLAMAV_ENABLE}
|
||||||
|
OPENDMARC_ENABLE=${OPENDMARC_ENABLE}
|
||||||
|
FAIL2BAN_ENABLE=${FAIL2BAN_ENABLE}
|
||||||
|
|
||||||
|
BACKUP_ONCALENDAR="${BACKUP_ONCALENDAR:-*-*-* 03:00:00}"
|
||||||
|
BACKUP_ENABLED=0
|
||||||
|
BACKUP_INTERVAL=daily
|
||||||
|
BACKUP_RETENTION_DAYS=7
|
||||||
|
BACKUP_DIR=/var/backups/mailwolt
|
||||||
|
BACKUP_USE_ZSTD=1
|
||||||
|
EOF
|
||||||
|
chmod 600 /etc/mailwolt/installer.env
|
||||||
|
|
||||||
|
# ── Installer-Sequenz ────────────────────────────────────────
|
||||||
|
for STEP in \
|
||||||
|
10-provision \
|
||||||
|
20-ssl 21-le-deploy-hook 22-dkim-helper \
|
||||||
|
30-db 40-postfix 50-dovecot \
|
||||||
|
60-rspamd-opendkim 61-opendmarc 62-clamav 63-fail2ban 64-apply-milters \
|
||||||
|
70-nginx 75-le-issue 80-app 88-update-wrapper 90-services \
|
||||||
|
92-sudoers-npm 93-backup-tools 95-woltguard 98-motd 99-summary
|
||||||
|
do
|
||||||
|
log ">>> Running ${STEP}.sh"
|
||||||
|
bash "./${STEP}.sh"
|
||||||
|
done
|
||||||
|
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#set -euo pipefail
|
||||||
|
#
|
||||||
|
## --- Flags / Modi ---
|
||||||
|
#DEV_MODE=0
|
||||||
|
#PROXY_MODE=0
|
||||||
|
#NPM_IP=""
|
||||||
|
#
|
||||||
|
#while [[ $# -gt 0 ]]; do
|
||||||
|
# case "$1" in
|
||||||
|
# -dev) DEV_MODE=1 ;;
|
||||||
|
# -proxy) PROXY_MODE=1; NPM_IP="${2:-}"; shift ;;
|
||||||
|
# esac
|
||||||
|
# shift
|
||||||
|
#done
|
||||||
|
#
|
||||||
|
#APP_ENV="${APP_ENV:-$([[ $DEV_MODE -eq 1 ]] && echo local || echo production)}"
|
||||||
|
#APP_DEBUG="${APP_DEBUG:-$([[ $DEV_MODE -eq 1 ]] && echo true || echo false)}"
|
||||||
|
#export DEV_MODE PROXY_MODE NPM_IP APP_ENV APP_DEBUG
|
||||||
|
#
|
||||||
|
#DB_PASS="${DB_PASS:-$(openssl rand -hex 16)}"
|
||||||
|
#REDIS_PASS="${REDIS_PASS:-$(openssl rand -hex 16)}"
|
||||||
|
#
|
||||||
|
#export DB_PASS REDIS_PASS
|
||||||
|
#
|
||||||
|
#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}"
|
||||||
|
#
|
||||||
|
#DB_NAME="${DB_NAME:-${APP_USER}}"
|
||||||
|
#DB_USER="${DB_USER:-${APP_USER}}"
|
||||||
|
#
|
||||||
|
#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}"
|
||||||
|
#
|
||||||
|
## ── Schöne, farbige Abfragen ────────────────────────────────────────────────
|
||||||
|
#echo -e "${CYAN}"
|
||||||
|
#echo "──────────────────────────────────────────────"
|
||||||
|
#echo -e " 📧 MailWolt Setup – Domain Konfiguration"
|
||||||
|
#echo "──────────────────────────────────────────────"
|
||||||
|
#echo -e "${NC}"
|
||||||
|
#
|
||||||
|
#MTA_DEFAULT="${MTA_SUB}.${BASE_DOMAIN}"
|
||||||
|
#UI_DEFAULT="${UI_SUB}.${BASE_DOMAIN}"
|
||||||
|
#WEBMAIL_DEFAULT="${WEBMAIL_SUB}.${BASE_DOMAIN}"
|
||||||
|
#
|
||||||
|
#ask_domain() {
|
||||||
|
# local __outvar="$1" label="$2" example="$3" defval="$4" input=""
|
||||||
|
# echo -e "${GREEN}[?]${NC} ${label}"
|
||||||
|
# echo -e " z.B. ${YELLOW}${example}${NC}"
|
||||||
|
# echo -e " Default: ${CYAN}${defval}${NC}"
|
||||||
|
# echo -ne " → Eingabe: ${CYAN}"
|
||||||
|
# read -r input
|
||||||
|
# echo -e "${NC}"
|
||||||
|
# if [[ -z "$input" ]]; then
|
||||||
|
# eval "$__outvar='$defval'"
|
||||||
|
# else
|
||||||
|
# eval "$__outvar='$input'"
|
||||||
|
# fi
|
||||||
|
#}
|
||||||
|
#
|
||||||
|
#ask_toggle() {
|
||||||
|
# local __outvar="$1" label="$2" defval="${3:-1}" input=""
|
||||||
|
# echo -ne "${GREEN}[?]${NC} ${label} (${CYAN}1${NC}=Ja / ${YELLOW}0${NC}=Nein) [Enter=${defval}]: "
|
||||||
|
# read -r input
|
||||||
|
# input="${input:-$defval}"
|
||||||
|
# case "$input" in
|
||||||
|
# 1|0) ;;
|
||||||
|
# *) echo -e "${YELLOW}Ungültig, nehme Default=${defval}.${NC}"; input="$defval" ;;
|
||||||
|
# esac
|
||||||
|
# eval "$__outvar='$input'"
|
||||||
|
#}
|
||||||
|
#
|
||||||
|
#ask_domain "MTA_FQDN" "Mailserver-FQDN (MX)" "mx.domain.tld" "$MTA_DEFAULT"
|
||||||
|
#ask_domain "UI_FQDN" "UI / Admin-Panel" "ui.domain.tld" "$UI_DEFAULT"
|
||||||
|
#ask_domain "WEBMAIL_FQDN" "Webmail-FQDN" "webmail.domain.tld" "$WEBMAIL_DEFAULT"
|
||||||
|
#
|
||||||
|
#echo -e "${CYAN}"
|
||||||
|
#echo "──────────────────────────────────────────────"
|
||||||
|
#echo -e " 🛡 Optionale Dienste"
|
||||||
|
#echo "──────────────────────────────────────────────"
|
||||||
|
#echo -e "${NC}"
|
||||||
|
#
|
||||||
|
#ask_toggle "CLAMAV_ENABLE" "ClamAV Virenscan aktivieren?" 1
|
||||||
|
#ask_toggle "OPENDMARC_ENABLE" "OpenDMARC auswerten?" 1
|
||||||
|
#ask_toggle "FAIL2BAN_ENABLE" "Fail2Ban aktivieren?" 1
|
||||||
|
#echo
|
||||||
|
#
|
||||||
|
## Defaults, wenn Enter gedrückt (Abwärtskompatibilität)
|
||||||
|
#MTA_FQDN="${MTA_FQDN:-${MTA_SUB}.${BASE_DOMAIN}}"
|
||||||
|
#UI_FQDN="${UI_FQDN:-${UI_SUB}.${BASE_DOMAIN}}"
|
||||||
|
#WEBMAIL_FQDN="${WEBMAIL_FQDN:-${WEBMAIL_SUB}.${BASE_DOMAIN}}"
|
||||||
|
#DKIM_ENABLE="${DKIM_ENABLE:-1}"
|
||||||
|
#DKIM_SELECTOR="${DKIM_SELECTOR:-mwl1}"
|
||||||
|
#DKIM_GENERATE="${DKIM_GENERATE:-1}"
|
||||||
|
#
|
||||||
|
## BASE_DOMAIN und Sub-Labels aus MTA/UI/WEBMAIL ableiten (robust)
|
||||||
|
#if [[ "$MTA_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then
|
||||||
|
# MTA_SUB="${BASH_REMATCH[1]}"
|
||||||
|
# BASE_DOMAIN="${BASH_REMATCH[2]}"
|
||||||
|
#fi
|
||||||
|
#if [[ "$UI_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then
|
||||||
|
# UI_SUB="${BASH_REMATCH[1]}"
|
||||||
|
#fi
|
||||||
|
#if [[ "$WEBMAIL_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then
|
||||||
|
# WEBMAIL_SUB="${BASH_REMATCH[1]}"
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
#SYSMAIL_SUB="${SYSMAIL_SUB:-sysmail}"
|
||||||
|
#SYSMAIL_DOMAIN="${SYSMAIL_SUB}.${BASE_DOMAIN}"
|
||||||
|
## Kanonische Host-Variablen (NIE wieder zusammenbauen – nimm die FQDNs)
|
||||||
|
#MAIL_HOSTNAME="${MTA_FQDN}"
|
||||||
|
#UI_HOST="${UI_FQDN}"
|
||||||
|
#WEBMAIL_HOST="${WEBMAIL_FQDN}"
|
||||||
|
#
|
||||||
|
## Zeitzone/Locale sinnvoll setzen
|
||||||
|
#APP_TZ="${APP_TZ:-$DEFAULT_TZ}"
|
||||||
|
#APP_LOCALE="${APP_LOCALE:-$DEFAULT_LOCALE}"
|
||||||
|
#
|
||||||
|
## ── Variablen exportieren ───────────────────────────────────────────────────
|
||||||
|
#export APP_NAME APP_USER APP_GROUP APP_USER_PREFIX APP_DIR
|
||||||
|
#export BASE_DOMAIN UI_SUB WEBMAIL_SUB MTA_SUB
|
||||||
|
#export SYSMAIL_SUB SYSMAIL_DOMAIN DKIM_ENABLE DKIM_SELECTOR DKIM_GENERATE
|
||||||
|
#export UI_HOST WEBMAIL_HOST MAIL_HOSTNAME
|
||||||
|
#export DB_NAME DB_USER
|
||||||
|
#export SERVER_PUBLIC_IPV4 SERVER_PUBLIC_IPV6 APP_TZ APP_LOCALE
|
||||||
|
#export CLAMAV_ENABLE OPENDMARC_ENABLE FAIL2BAN_ENABLE
|
||||||
|
#
|
||||||
|
#install -d -m 0755 /etc/mailwolt
|
||||||
|
#cat >/etc/mailwolt/installer.env <<EOF
|
||||||
|
#BASE_DOMAIN=${BASE_DOMAIN}
|
||||||
|
#MTA_SUB=${MTA_SUB}
|
||||||
|
#UI_SUB=${UI_SUB}
|
||||||
|
#WEBMAIL_SUB=${WEBMAIL_SUB}
|
||||||
|
#
|
||||||
|
#MAIL_HOSTNAME=${MAIL_HOSTNAME}
|
||||||
|
#UI_HOST=${UI_HOST}
|
||||||
|
#WEBMAIL_HOST=${WEBMAIL_HOST}
|
||||||
|
#
|
||||||
|
#SYSMAIL_SUB=${SYSMAIL_SUB}
|
||||||
|
#SYSMAIL_DOMAIN=${SYSMAIL_DOMAIN}
|
||||||
|
#
|
||||||
|
#DKIM_ENABLE=${DKIM_ENABLE}
|
||||||
|
#DKIM_SELECTOR=${DKIM_SELECTOR}
|
||||||
|
#DKIM_GENERATE=${DKIM_GENERATE}
|
||||||
|
#
|
||||||
|
#DB_HOST=127.0.0.1
|
||||||
|
#DB_NAME=${DB_NAME}
|
||||||
|
#DB_USER=${DB_USER}
|
||||||
|
#DB_PASS=${DB_PASS}
|
||||||
|
#REDIS_PASS=${REDIS_PASS}
|
||||||
|
#
|
||||||
|
#SERVER_PUBLIC_IPV4=${SERVER_PUBLIC_IPV4}
|
||||||
|
#SERVER_PUBLIC_IPV6=${SERVER_PUBLIC_IPV6}
|
||||||
|
#APP_ENV=${APP_ENV}
|
||||||
|
#
|
||||||
|
#CLAMAV_ENABLE=${CLAMAV_ENABLE}
|
||||||
|
#OPENDMARC_ENABLE=${OPENDMARC_ENABLE}
|
||||||
|
#FAIL2BAN_ENABLE=${FAIL2BAN_ENABLE}
|
||||||
|
#EOF
|
||||||
|
#
|
||||||
|
#chmod 600 /etc/mailwolt/installer.env
|
||||||
|
#
|
||||||
|
## ── Sequenz ────────────────────────────────────────────────────────────────
|
||||||
|
#for STEP in 10-provision 20-ssl 21-le-deploy-hook 22-dkim-helper 30-db 40-postfix 50-dovecot 60-rspamd-opendkim 61-opendmarc 62-clamav 63-fail2ban 70-nginx 75-le-issue 80-app 90-services 95-woltguard 98-motd 99-summary
|
||||||
|
#do
|
||||||
|
# log ">>> Running ${STEP}.sh"
|
||||||
|
# bash "./${STEP}.sh"
|
||||||
|
#done
|
||||||
|
###!/usr/bin/env bash
|
||||||
|
##set -euo pipefail
|
||||||
|
##
|
||||||
|
### --- Flags / Modi ---
|
||||||
|
##DEV_MODE=0
|
||||||
|
##PROXY_MODE=0
|
||||||
|
##NPM_IP=""
|
||||||
|
##
|
||||||
|
##while [[ $# -gt 0 ]]; do
|
||||||
|
## case "$1" in
|
||||||
|
## -dev) DEV_MODE=1 ;;
|
||||||
|
## -proxy) PROXY_MODE=1; NPM_IP="${2:-}"; shift ;;
|
||||||
|
## esac
|
||||||
|
## shift
|
||||||
|
##done
|
||||||
|
##
|
||||||
|
##APP_ENV="${APP_ENV:-$([[ $DEV_MODE -eq 1 ]] && echo local || echo production)}"
|
||||||
|
##APP_DEBUG="${APP_DEBUG:-$([[ $DEV_MODE -eq 1 ]] && echo true || echo false)}"
|
||||||
|
##export DEV_MODE PROXY_MODE NPM_IP APP_ENV APP_DEBUG
|
||||||
|
##
|
||||||
|
##DB_PASS="${DB_PASS:-$(openssl rand -hex 16)}"
|
||||||
|
##REDIS_PASS="${REDIS_PASS:-$(openssl rand -hex 16)}"
|
||||||
|
##
|
||||||
|
##export DB_PASS REDIS_PASS
|
||||||
|
##
|
||||||
|
##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}"
|
||||||
|
##
|
||||||
|
##DB_NAME="${DB_NAME:-${APP_USER}}"
|
||||||
|
##DB_USER="${DB_USER:-${APP_USER}}"
|
||||||
|
##
|
||||||
|
##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}"
|
||||||
|
##
|
||||||
|
### ── FQDNs abfragen ───────────────────────────────────────────────────────────
|
||||||
|
##read -r -p "Mailserver FQDN (MX, z.B. mx.domain.tld) [Enter=${MTA_SUB}.${BASE_DOMAIN}]: " MTA_FQDN
|
||||||
|
##read -r -p "UI / Admin-Panel FQDN (z.B. ui.domain.tld) [Enter=${UI_SUB}.${BASE_DOMAIN}]: " UI_FQDN
|
||||||
|
##read -r -p "Webmail FQDN (z.B. webmail.domain.tld) [Enter=${WEBMAIL_SUB}.${BASE_DOMAIN}]: " WEBMAIL_FQDN
|
||||||
|
##
|
||||||
|
### Defaults, wenn Enter gedrückt
|
||||||
|
##MTA_FQDN="${MTA_FQDN:-${MTA_SUB}.${BASE_DOMAIN}}"
|
||||||
|
##UI_FQDN="${UI_FQDN:-${UI_SUB}.${BASE_DOMAIN}}"
|
||||||
|
##WEBMAIL_FQDN="${WEBMAIL_FQDN:-${WEBMAIL_SUB}.${BASE_DOMAIN}}"
|
||||||
|
##DKIM_ENABLE="${DKIM_ENABLE:-1}"
|
||||||
|
##DKIM_SELECTOR="${DKIM_SELECTOR:-mwl1}"
|
||||||
|
##DKIM_GENERATE="${DKIM_GENERATE:-1}"
|
||||||
|
##
|
||||||
|
### BASE_DOMAIN und Sub-Labels aus MTA/UI/WEBMAIL ableiten (robust)
|
||||||
|
##if [[ "$MTA_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then
|
||||||
|
## MTA_SUB="${BASH_REMATCH[1]}"
|
||||||
|
## BASE_DOMAIN="${BASH_REMATCH[2]}"
|
||||||
|
##fi
|
||||||
|
##if [[ "$UI_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then
|
||||||
|
## UI_SUB="${BASH_REMATCH[1]}"
|
||||||
|
##fi
|
||||||
|
##if [[ "$WEBMAIL_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then
|
||||||
|
## WEBMAIL_SUB="${BASH_REMATCH[1]}"
|
||||||
|
##fi
|
||||||
|
##
|
||||||
|
##SYSMAIL_SUB="${SYSMAIL_SUB:-sysmail}"
|
||||||
|
##SYSMAIL_DOMAIN="${SYSMAIL_SUB}.${BASE_DOMAIN}"
|
||||||
|
### Kanonische Host-Variablen (NIE wieder zusammenbauen – nimm die FQDNs)
|
||||||
|
##MAIL_HOSTNAME="${MTA_FQDN}"
|
||||||
|
##UI_HOST="${UI_FQDN}"
|
||||||
|
##WEBMAIL_HOST="${WEBMAIL_FQDN}"
|
||||||
|
##
|
||||||
|
### Zeitzone/Locale sinnvoll setzen (könntest du auch noch abfragen)
|
||||||
|
##APP_TZ="${APP_TZ:-$DEFAULT_TZ}"
|
||||||
|
##APP_LOCALE="${APP_LOCALE:-$DEFAULT_LOCALE}"
|
||||||
|
##
|
||||||
|
### ── Variablen exportieren ───────────────────────────────────────────────────
|
||||||
|
##export APP_NAME APP_USER APP_GROUP APP_USER_PREFIX APP_DIR
|
||||||
|
##export BASE_DOMAIN UI_SUB WEBMAIL_SUB MTA_SUB
|
||||||
|
##export SYSMAIL_SUB SYSMAIL_DOMAIN DKIM_ENABLE DKIM_SELECTOR DKIM_GENERATE
|
||||||
|
##export UI_HOST WEBMAIL_HOST MAIL_HOSTNAME
|
||||||
|
##export DB_NAME DB_USER
|
||||||
|
##export SERVER_PUBLIC_IPV4 SERVER_PUBLIC_IPV6 APP_TZ APP_LOCALE
|
||||||
|
##
|
||||||
|
##install -d -m 0755 /etc/mailwolt
|
||||||
|
##cat >/etc/mailwolt/installer.env <<EOF
|
||||||
|
##BASE_DOMAIN=${BASE_DOMAIN}
|
||||||
|
##MTA_SUB=${MTA_SUB}
|
||||||
|
##UI_SUB=${UI_SUB}
|
||||||
|
##WEBMAIL_SUB=${WEBMAIL_SUB}
|
||||||
|
##
|
||||||
|
##MAIL_HOSTNAME=${MAIL_HOSTNAME}
|
||||||
|
##UI_HOST=${UI_HOST}
|
||||||
|
##WEBMAIL_HOST=${WEBMAIL_HOST}
|
||||||
|
##
|
||||||
|
##SYSMAIL_SUB=${SYSMAIL_SUB}
|
||||||
|
##SYSMAIL_DOMAIN=${SYSMAIL_DOMAIN}
|
||||||
|
##
|
||||||
|
##DKIM_ENABLE=${DKIM_ENABLE}
|
||||||
|
##DKIM_SELECTOR=${DKIM_SELECTOR}
|
||||||
|
##DKIM_GENERATE=${DKIM_GENERATE}
|
||||||
|
##
|
||||||
|
##DB_HOST=127.0.0.1
|
||||||
|
##DB_NAME=${DB_NAME}
|
||||||
|
##DB_USER=${DB_USER}
|
||||||
|
##DB_PASS=${DB_PASS}
|
||||||
|
##REDIS_PASS=${REDIS_PASS}
|
||||||
|
##
|
||||||
|
##SERVER_PUBLIC_IPV4=${SERVER_PUBLIC_IPV4}
|
||||||
|
##SERVER_PUBLIC_IPV6=${SERVER_PUBLIC_IPV6}
|
||||||
|
##APP_ENV=${APP_ENV}
|
||||||
|
##
|
||||||
|
##CLAMAV_ENABLE=1
|
||||||
|
##OPENDMARC_ENABLE=1
|
||||||
|
##FAIL2BAN_ENABLE=1
|
||||||
|
##EOF
|
||||||
|
##
|
||||||
|
##chmod 600 /etc/mailwolt/installer.env
|
||||||
|
##
|
||||||
|
### ── Sequenz ────────────────────────────────────────────────────────────────
|
||||||
|
##for STEP in 10-provision 20-ssl 21-le-deploy-hook 22-dkim-helper 30-db 40-postfix 50-dovecot 60-rspamd-opendkim 61-opendmarc 62-clamav 63-fail2ban 70-nginx 75-le-issue 80-app 90-services 95-woltguard 98-motd 99-summary
|
||||||
|
##do
|
||||||
|
## log ">>> Running ${STEP}.sh"
|
||||||
|
## bash "./${STEP}.sh"
|
||||||
|
##done
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Mailwolt Installer-Wrapper
|
||||||
|
# Deploy to: /usr/local/sbin/mailwolt-install
|
||||||
|
# Permissions: chmod 0755, chown root:root
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
LOG="/var/log/mailwolt-install.log"
|
||||||
|
STATE_DIR="/var/lib/mailwolt/install"
|
||||||
|
INSTALLER_SCRIPT="/var/www/mailwolt/mailwolt-installer/install.sh"
|
||||||
|
APP_DIR="/var/www/mailwolt"
|
||||||
|
|
||||||
|
install -d -m 0755 "$STATE_DIR" /var/lib/mailwolt /var/lib/mailwolt/wizard
|
||||||
|
chown www-data:www-data /var/lib/mailwolt/wizard
|
||||||
|
: > "$LOG"
|
||||||
|
chmod 0644 "$LOG"
|
||||||
|
|
||||||
|
echo "running" > "$STATE_DIR/state"
|
||||||
|
: > "$STATE_DIR/rc"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "===== $(date -Is) :: Installation gestartet ====="
|
||||||
|
|
||||||
|
if [[ "$(id -u)" -ne 0 ]]; then
|
||||||
|
echo "[!] Muss als root laufen"
|
||||||
|
printf '1\n' > "$STATE_DIR/rc"
|
||||||
|
echo "done" > "$STATE_DIR/state"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Komponente aus $1, falls übergeben (z.B. "nginx", "postfix", "dovecot", "all")
|
||||||
|
COMPONENT="${1:-all}"
|
||||||
|
echo "[i] Komponente: $COMPONENT"
|
||||||
|
|
||||||
|
RC=0
|
||||||
|
if [[ -f "$INSTALLER_SCRIPT" ]]; then
|
||||||
|
APP_DIR="$APP_DIR" COMPONENT="$COMPONENT" bash "$INSTALLER_SCRIPT" || RC=$?
|
||||||
|
else
|
||||||
|
echo "[!] installer script nicht gefunden: $INSTALLER_SCRIPT"
|
||||||
|
RC=127
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "===== $(date -Is) :: Installation beendet (rc=$RC) ====="
|
||||||
|
printf '%s\n' "$RC" > "$STATE_DIR/rc"
|
||||||
|
echo "done" > "$STATE_DIR/state"
|
||||||
|
exit "$RC"
|
||||||
|
} 2>&1 | tee -a "$LOG"
|
||||||
|
|
@ -0,0 +1,189 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [ -f /etc/mailwolt/installer.env ]; then
|
||||||
|
set -a
|
||||||
|
. /etc/mailwolt/installer.env
|
||||||
|
set +a
|
||||||
|
fi
|
||||||
|
# ── 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."; }
|
||||||
|
|
||||||
|
# --- Defaults, nur wenn noch nicht gesetzt ---------------------------------
|
||||||
|
: "${APP_USER:=mailwolt}"
|
||||||
|
: "${APP_GROUP:=www-data}"
|
||||||
|
: "${APP_DIR:=/var/www/${APP_USER}}"
|
||||||
|
|
||||||
|
: "${APP_NAME:=MailWolt}"
|
||||||
|
|
||||||
|
: "${BASE_DOMAIN:=example.com}"
|
||||||
|
: "${UI_SUB:=ui}"
|
||||||
|
: "${WEBMAIL_SUB:=webmail}"
|
||||||
|
: "${MTA_SUB:=mx}"
|
||||||
|
|
||||||
|
# DB / Redis (werden später durch .env überschrieben)
|
||||||
|
: "${DB_NAME:=${APP_USER}}"
|
||||||
|
: "${DB_USER:=${APP_USER}}"
|
||||||
|
: "${DB_PASS:=}"
|
||||||
|
: "${REDIS_PASS:=}"
|
||||||
|
|
||||||
|
# Stabile Zert-Pfade (UI/WEBMAIL/MX → symlinked via 20-ssl.sh)
|
||||||
|
: "${MAIL_SSL_DIR:=/etc/ssl/mail}"
|
||||||
|
: "${UI_SSL_DIR:=/etc/ssl/ui}"
|
||||||
|
: "${WEBMAIL_SSL_DIR:=/etc/ssl/webmail}"
|
||||||
|
: "${UI_CERT:=${UI_SSL_DIR}/fullchain.pem}"
|
||||||
|
: "${UI_KEY:=${UI_SSL_DIR}/privkey.pem}"
|
||||||
|
|
||||||
|
# Optional: E-Mail für LE
|
||||||
|
: "${LE_EMAIL:=admin@${BASE_DOMAIN}}"
|
||||||
|
|
||||||
|
load_env_file(){
|
||||||
|
local f="$1"
|
||||||
|
[[ -f "$f" ]] || return 0
|
||||||
|
while IFS='=' read -r k v; do
|
||||||
|
[[ "$k" =~ ^[A-Z0-9_]+$ ]] || continue
|
||||||
|
export "$k=$v"
|
||||||
|
done < <(grep -E '^[A-Z0-9_]+=' "$f")
|
||||||
|
}
|
||||||
|
|
||||||
|
header(){ echo -e "${CYAN}${BAR}${NC}
|
||||||
|
${CYAN} :::: :::: ::: ::::::::::: ::: ::: ::: :::::::: ::: :::::::::::
|
||||||
|
${CYAN} +:+:+: :+:+:+ :+: :+: :+: :+: :+: :+: :+: :+: :+: :+:
|
||||||
|
${CYAN} +:+ +:+:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+
|
||||||
|
${CYAN} +#+ +:+ +#+ +#++:++#++: +#+ +#+ +#+ +:+ +#+ +#+ +:+ +#+ +#+
|
||||||
|
${CYAN} +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+#+ +#+ +#+ +#+ +#+ +#+
|
||||||
|
${CYAN} #+# #+# #+# #+# #+# #+# #+#+# #+#+# #+# #+# #+# #+#
|
||||||
|
${CYAN} ### ### ### ### ########### ########## ### ### ######## ########## ###
|
||||||
|
${CYAN} ${CYAN}${BAR}${NC}\n"; }
|
||||||
|
|
||||||
|
#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_ipv4() {
|
||||||
|
local ext=""
|
||||||
|
if command -v curl >/dev/null 2>&1; then
|
||||||
|
ext="$(curl -fsS --max-time 2 https://ifconfig.me 2>/dev/null || true)"
|
||||||
|
[[ "$ext" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || ext=""
|
||||||
|
fi
|
||||||
|
echo "$ext"
|
||||||
|
}
|
||||||
|
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"; }
|
||||||
|
|
||||||
|
# dns_preflight HOST [HOST2 ...]
|
||||||
|
# Prüft: A-Record → SERVER_PUBLIC_IPV4, MX (nur wenn HOST == MAIL_HOSTNAME), PTR.
|
||||||
|
# Gibt strukturierte Zeilen aus: OK|WARN|FAIL <host> <check> <detail>
|
||||||
|
# Rückgabe 0 = alles OK; 1 = mind. ein FAIL.
|
||||||
|
dns_preflight(){
|
||||||
|
local overall=0
|
||||||
|
local server_ip="${SERVER_PUBLIC_IPV4:-}"
|
||||||
|
|
||||||
|
_dns_line(){ local level="$1" host="$2" check="$3" detail="$4"
|
||||||
|
case "$level" in
|
||||||
|
OK) echo -e "${GREEN}[DNS OK ]${NC} ${host} ${GREY}${check}${NC} → ${detail}" ;;
|
||||||
|
WARN) echo -e "${YELLOW}[DNS WARN]${NC} ${host} ${GREY}${check}${NC} → ${detail}" ;;
|
||||||
|
FAIL) echo -e "${RED}[DNS FAIL]${NC} ${host} ${GREY}${check}${NC} → ${detail}"; overall=1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
for host in "$@"; do
|
||||||
|
[[ -z "$host" || "$host" == "example.com" ]] && continue
|
||||||
|
|
||||||
|
# A-Record
|
||||||
|
local a_ip
|
||||||
|
a_ip="$(dig +short A "$host" @1.1.1.1 2>/dev/null | grep -Eo '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -n1)"
|
||||||
|
if [[ -z "$a_ip" ]]; then
|
||||||
|
_dns_line FAIL "$host" "A-Record" "kein Eintrag gefunden"
|
||||||
|
elif [[ -n "$server_ip" && "$a_ip" != "$server_ip" ]]; then
|
||||||
|
_dns_line FAIL "$host" "A-Record" "${a_ip} ≠ ${server_ip} (Server-IP)"
|
||||||
|
else
|
||||||
|
_dns_line OK "$host" "A-Record" "${a_ip}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# MX (nur für MAIL_HOSTNAME)
|
||||||
|
if [[ "$host" == "${MAIL_HOSTNAME:-}" ]]; then
|
||||||
|
local mx
|
||||||
|
mx="$(dig +short MX "$host" @1.1.1.1 2>/dev/null | awk '{print $2}' | head -n1)"
|
||||||
|
if [[ -z "$mx" ]]; then
|
||||||
|
_dns_line WARN "$host" "MX-Record" "kein Eintrag – ausgehende Mail ggf. eingeschränkt"
|
||||||
|
else
|
||||||
|
_dns_line OK "$host" "MX-Record" "${mx}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# PTR (nur wenn IP bekannt)
|
||||||
|
if [[ -n "$server_ip" && -n "$a_ip" && "$a_ip" == "$server_ip" ]]; then
|
||||||
|
local ptr
|
||||||
|
ptr="$(dig +short -x "$server_ip" @1.1.1.1 2>/dev/null | head -n1 | sed 's/\.$//')"
|
||||||
|
if [[ -z "$ptr" ]]; then
|
||||||
|
_dns_line WARN "$host" "PTR-Record" "kein Reverse-DNS – kann Spam-Score erhöhen"
|
||||||
|
elif [[ "$ptr" != "$host" && "$ptr" != "${MAIL_HOSTNAME:-}" ]]; then
|
||||||
|
_dns_line WARN "$host" "PTR-Record" "${ptr} (zeigt nicht auf ${host})"
|
||||||
|
else
|
||||||
|
_dns_line OK "$host" "PTR-Record" "${ptr}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
return $overall
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Phase 2: Go-Live — DNS-Check → LE-Zertifikate → Nginx → Postfix/Dovecot aktualisieren
|
||||||
|
# Muss als root ausgeführt werden, NACHDEM die DNS-Einträge gesetzt wurden.
|
||||||
|
# Liest Konfiguration aus /etc/mailwolt/installer.env (durch Phase 1 geschrieben).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source "${SCRIPT_DIR}/lib.sh"
|
||||||
|
|
||||||
|
require_root
|
||||||
|
|
||||||
|
ENV_FILE="/etc/mailwolt/installer.env"
|
||||||
|
[[ -f "$ENV_FILE" ]] || die "Phase 1 noch nicht abgeschlossen – ${ENV_FILE} fehlt."
|
||||||
|
|
||||||
|
set -a; . "$ENV_FILE"; set +a
|
||||||
|
|
||||||
|
# ── Pflichtfelder ─────────────────────────────────────────────────────────────
|
||||||
|
: "${BASE_DOMAIN:?BASE_DOMAIN fehlt in ${ENV_FILE}}"
|
||||||
|
: "${UI_HOST:?UI_HOST fehlt in ${ENV_FILE}}"
|
||||||
|
: "${WEBMAIL_HOST:?WEBMAIL_HOST fehlt in ${ENV_FILE}}"
|
||||||
|
: "${MAIL_HOSTNAME:?MAIL_HOSTNAME fehlt in ${ENV_FILE}}"
|
||||||
|
: "${SERVER_PUBLIC_IPV4:?SERVER_PUBLIC_IPV4 fehlt in ${ENV_FILE}}"
|
||||||
|
|
||||||
|
header
|
||||||
|
echo -e "${CYAN} Phase 2 – Go-Live${NC}"
|
||||||
|
echo -e "${CYAN}${BAR}${NC}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# ── Schritt 1: DNS Preflight ──────────────────────────────────────────────────
|
||||||
|
log "DNS-Vorab-Check …"
|
||||||
|
|
||||||
|
SKIP_DNS="${SKIP_DNS:-0}"
|
||||||
|
dns_ok=0
|
||||||
|
|
||||||
|
if [[ "$SKIP_DNS" != "1" ]]; then
|
||||||
|
if dns_preflight "$UI_HOST" "$WEBMAIL_HOST" "$MAIL_HOSTNAME"; then
|
||||||
|
dns_ok=1
|
||||||
|
else
|
||||||
|
warn "Einige DNS-Einträge zeigen noch nicht auf diesen Server."
|
||||||
|
echo
|
||||||
|
echo -e " Möglichkeiten:"
|
||||||
|
echo -e " a) DNS reparieren und dieses Skript erneut ausführen."
|
||||||
|
echo -e " b) Trotzdem fortfahren: SKIP_DNS=1 bash phase2-go-live.sh"
|
||||||
|
echo
|
||||||
|
read -rp "Trotzdem fortfahren? [j/N] " _ans
|
||||||
|
[[ "${_ans,,}" == "j" ]] || die "Abgebrochen."
|
||||||
|
dns_ok=1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "DNS-Check übersprungen (SKIP_DNS=1)."
|
||||||
|
dns_ok=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
|
||||||
|
# ── Schritt 2: Let's Encrypt Zertifikate ─────────────────────────────────────
|
||||||
|
log "Let's Encrypt Zertifikate ausstellen …"
|
||||||
|
bash "${SCRIPT_DIR}/75-le-issue.sh"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# ── Schritt 3: Nginx-Konfiguration neu schreiben (TLS) ───────────────────────
|
||||||
|
log "Nginx-Konfiguration aktualisieren (TLS) …"
|
||||||
|
# Nginx-Builder aus 70-nginx.sh wiederverwenden
|
||||||
|
source "${SCRIPT_DIR}/70-nginx.sh" || true # sourcing setzt Variablen und führt aus
|
||||||
|
echo
|
||||||
|
|
||||||
|
# ── Schritt 4: Postfix hostname + TLS-Zertifikate aktualisieren ──────────────
|
||||||
|
log "Postfix: myhostname = ${MAIL_HOSTNAME} …"
|
||||||
|
postconf -e "myhostname = ${MAIL_HOSTNAME}"
|
||||||
|
postconf -e "myorigin = \$myhostname"
|
||||||
|
postconf -e "smtpd_tls_cert_file = /etc/ssl/mail/fullchain.pem"
|
||||||
|
postconf -e "smtpd_tls_key_file = /etc/ssl/mail/privkey.pem"
|
||||||
|
systemctl reload postfix || true
|
||||||
|
|
||||||
|
# ── Schritt 5: Dovecot TLS ───────────────────────────────────────────────────
|
||||||
|
log "Dovecot: TLS-Zertifikate aktualisieren …"
|
||||||
|
if [[ -f /etc/dovecot/conf.d/10-ssl.conf ]]; then
|
||||||
|
sed -i "s|^ssl_cert =.*|ssl_cert = </etc/ssl/mail/fullchain.pem|" /etc/dovecot/conf.d/10-ssl.conf || true
|
||||||
|
sed -i "s|^ssl_key =.*|ssl_key = </etc/ssl/mail/privkey.pem|" /etc/dovecot/conf.d/10-ssl.conf || true
|
||||||
|
systemctl reload dovecot || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Schritt 6: App-URL in .env aktualisieren ──────────────────────────────────
|
||||||
|
APP_ENV_FILE="${APP_DIR}/.env"
|
||||||
|
if [[ -f "$APP_ENV_FILE" ]]; then
|
||||||
|
log "Laravel APP_URL aktualisieren → https://${UI_HOST} …"
|
||||||
|
ENV_FILE="$APP_ENV_FILE"
|
||||||
|
upsert_env APP_URL "https://${UI_HOST}"
|
||||||
|
upsert_env APP_HOST "${UI_HOST}"
|
||||||
|
upsert_env SESSION_SECURE_COOKIE "true"
|
||||||
|
sudo -u "${APP_USER}" -H bash -lc "cd ${APP_DIR} && php artisan optimize:clear && php artisan config:cache" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Fertig ────────────────────────────────────────────────────────────────────
|
||||||
|
echo
|
||||||
|
echo -e "${GREEN}${BAR}${NC}"
|
||||||
|
echo -e "${GREEN} ✔ Phase 2 abgeschlossen!${NC}"
|
||||||
|
echo -e "${GREEN}${BAR}${NC}"
|
||||||
|
echo -e " UI: ${CYAN}https://${UI_HOST}${NC}"
|
||||||
|
echo -e " Webmail: ${CYAN}https://${WEBMAIL_HOST}${NC}"
|
||||||
|
echo -e " MX: ${GREY}${MAIL_HOSTNAME}${NC}"
|
||||||
|
echo
|
||||||
|
|
@ -0,0 +1,261 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# -------- Konfiguration -------------------------------------------------------
|
||||||
|
APP_USER="${APP_USER:-mailwolt}"
|
||||||
|
APP_GROUP="${APP_GROUP:-www-data}" # Fallback; setz das in deiner Umgebung falls anders
|
||||||
|
APP_DIR="${APP_DIR:-/var/www/mailwolt}"
|
||||||
|
BRANCH="${BRANCH:-main}" # nur relevant bei UPDATE_MODE=branch
|
||||||
|
MODE="${UPDATE_MODE:-tags}" # tags | branch
|
||||||
|
ALLOW_DIRTY="${ALLOW_DIRTY:-0}" # 1 = Dirty-Working-Tree zulassen
|
||||||
|
|
||||||
|
# npm / CI Defaults für weniger Lärm
|
||||||
|
export CI=1
|
||||||
|
export NPM_CONFIG_FUND=false
|
||||||
|
export NPM_CONFIG_AUDIT=false
|
||||||
|
export npm_config_loglevel=warn
|
||||||
|
|
||||||
|
# -------- Helper --------------------------------------------------------------
|
||||||
|
as_app(){ sudo -u "$APP_USER" -H bash -lc "$*"; }
|
||||||
|
|
||||||
|
restart_if_exists(){
|
||||||
|
local u="$1"
|
||||||
|
systemctl list-unit-files | grep -q "^${u}\.service" && systemctl restart "$u" || true
|
||||||
|
}
|
||||||
|
|
||||||
|
reload_if_active(){
|
||||||
|
local u="$1"
|
||||||
|
systemctl is-active --quiet "$u" && systemctl reload "$u" || true
|
||||||
|
}
|
||||||
|
|
||||||
|
restart_php_fpm(){
|
||||||
|
for u in php8.3-fpm php8.2-fpm php8.1-fpm php-fpm; do
|
||||||
|
if systemctl list-unit-files | grep -q "^${u}\.service"; then
|
||||||
|
# reload = graceful: laufende Requests fertig, dann Workers tauschen
|
||||||
|
systemctl reload "$u" 2>/dev/null || systemctl restart "$u"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
artisan_down(){
|
||||||
|
as_app "cd ${APP_DIR} && php artisan down --retry=10 --render=errors.503" 2>/dev/null || true
|
||||||
|
echo "[i] Wartungsmodus aktiviert"
|
||||||
|
}
|
||||||
|
|
||||||
|
artisan_up(){
|
||||||
|
as_app "cd ${APP_DIR} && php artisan up" 2>/dev/null || true
|
||||||
|
echo "[i] Wartungsmodus beendet"
|
||||||
|
}
|
||||||
|
|
||||||
|
git_safe(){
|
||||||
|
# Falls nötig: Repo als safe markieren (z.B. wenn Root den Wrapper aufruft)
|
||||||
|
as_app "git -C ${APP_DIR} config --global --add safe.directory ${APP_DIR} >/dev/null 2>&1 || true"
|
||||||
|
}
|
||||||
|
|
||||||
|
git_dirty_check(){
|
||||||
|
if [[ "$ALLOW_DIRTY" != "1" ]]; then
|
||||||
|
local dirty
|
||||||
|
dirty="$(as_app "git -C ${APP_DIR} status --porcelain")"
|
||||||
|
if [[ -n "$dirty" ]]; then
|
||||||
|
echo "[!] Arbeitsbaum hat uncommitted Änderungen. Abbruch (ALLOW_DIRTY=1 zum Überschreiben)."
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
get_version(){
|
||||||
|
as_app "cd ${APP_DIR} && (git describe --tags --always 2>/dev/null || git rev-parse --short=7 HEAD)"
|
||||||
|
}
|
||||||
|
|
||||||
|
write_build_info(){
|
||||||
|
local ver="$1" rev="$2"
|
||||||
|
install -d -m 0755 /etc/mailwolt || true
|
||||||
|
printf "version=%s\nrev=%s\nupdated=%s\n" "$ver" "$rev" "$(date -Is)" > /etc/mailwolt/build.info || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Frontend build (quiet & robust) ------------------------------------------
|
||||||
|
frontend_build_quiet() {
|
||||||
|
local LOG="/var/log/mailwolt-frontend-build.log"
|
||||||
|
local NPM_ENV="CI=1 NPM_CONFIG_FUND=false NPM_CONFIG_AUDIT=false npm_config_loglevel=warn"
|
||||||
|
|
||||||
|
# Logfile vorbereiten
|
||||||
|
install -d -m 0755 "$(dirname "$LOG")"
|
||||||
|
: > "$LOG" || true
|
||||||
|
chmod 0644 "$LOG"
|
||||||
|
|
||||||
|
echo "[i] Frontend: vorbereiten …"
|
||||||
|
# Build-Ziele & Temp besitzbar machen
|
||||||
|
as_app "mkdir -p '${APP_DIR}/public/build' '${APP_DIR}/node_modules' '${APP_DIR}/.vite' '${APP_DIR}/.npm-cache'"
|
||||||
|
chown -R "$APP_USER":"$APP_GROUP" "${APP_DIR}/public/build" "${APP_DIR}/node_modules" "${APP_DIR}/.vite" "${APP_DIR}/.npm-cache" || true
|
||||||
|
chmod -R g+rwX "${APP_DIR}/public/build" "${APP_DIR}/node_modules" "${APP_DIR}/.vite" || true
|
||||||
|
|
||||||
|
# evtl. störrische Reste entfernen (vermeidet EACCES beim Unlink/Rimraf)
|
||||||
|
rm -rf "${APP_DIR}/node_modules/.vite" "${APP_DIR}/public/build/"* 2>/dev/null || true
|
||||||
|
|
||||||
|
# npm Config (kein Fund/Audit, eigener Cache unter App-User)
|
||||||
|
as_app "printf 'fund=false\naudit=false\ncache=${APP_DIR}/.npm-cache\n' > ~/.npmrc" >>"$LOG" 2>&1 || true
|
||||||
|
|
||||||
|
echo "[i] Frontend: Dependencies … (Details: $LOG)"
|
||||||
|
if ! as_app "cd '${APP_DIR}' && ${NPM_ENV} npm ci --no-audit --no-fund --no-progress" >>"$LOG" 2>&1; then
|
||||||
|
if ! as_app "cd '${APP_DIR}' && ${NPM_ENV} npm install --no-audit --no-fund --no-progress" >>"$LOG" 2>&1; then
|
||||||
|
echo "[!] npm install fehlgeschlagen. Letzte 60 Zeilen:"
|
||||||
|
tail -n 60 "$LOG" || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[i] Frontend: Build … (Details: $LOG)"
|
||||||
|
if ! as_app "cd '${APP_DIR}' && ${NPM_ENV} npm run build --silent --loglevel=warn" >>"$LOG" 2>&1; then
|
||||||
|
echo "[!] Build fehlgeschlagen. Letzte 80 Zeilen:"
|
||||||
|
tail -n 80 "$LOG" || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Nach dem Build noch einmal Rechte begradigen
|
||||||
|
chown -R "$APP_USER":"$APP_GROUP" "${APP_DIR}/public/build" || true
|
||||||
|
find "${APP_DIR}/public/build" -type d -exec chmod 2775 {} \; 2>/dev/null || true
|
||||||
|
find "${APP_DIR}/public/build" -type f -exec chmod 0664 {} \; 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "[✓] Frontend gebaut."
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------- Guards --------------------------------------------------------------
|
||||||
|
[[ "$(id -u)" -eq 0 ]] || { echo "[!] Bitte als root ausführen"; exit 1; }
|
||||||
|
[[ -d "$APP_DIR/.git" ]] || { echo "[!] $APP_DIR scheint kein Git-Repo zu sein"; exit 1; }
|
||||||
|
|
||||||
|
git_safe
|
||||||
|
git_dirty_check
|
||||||
|
|
||||||
|
# -------- Git: neuen Stand holen ---------------------------------------------
|
||||||
|
echo "[i] Prüfe Repository …"
|
||||||
|
OLD_REV="$(as_app "git -C ${APP_DIR} rev-parse HEAD")"
|
||||||
|
OLD_VER="$(get_version)"
|
||||||
|
NEW_REV="$OLD_REV"
|
||||||
|
|
||||||
|
if [[ "$MODE" = "tags" ]]; then
|
||||||
|
# → Neueste Tags holen
|
||||||
|
as_app "git -C ${APP_DIR} fetch --quiet origin && git -C ${APP_DIR} fetch --tags --quiet origin || true"
|
||||||
|
LATEST_TAG="$(as_app "git -C ${APP_DIR} tag --list | sort -V | tail -n1")"
|
||||||
|
if [[ -z "$LATEST_TAG" ]]; then
|
||||||
|
echo "[!] Keine Tags gefunden – falle auf origin/${BRANCH} zurück"
|
||||||
|
as_app "git -C ${APP_DIR} checkout -q ${BRANCH} && git -C ${APP_DIR} pull --ff-only origin ${BRANCH}"
|
||||||
|
else
|
||||||
|
TARGET_REV="$(as_app "git -C ${APP_DIR} rev-list -n1 ${LATEST_TAG}")"
|
||||||
|
if [[ "$TARGET_REV" = "$OLD_REV" ]]; then
|
||||||
|
echo "[✓] Bereits auf neuestem Release (${LATEST_TAG}) – nichts zu tun."
|
||||||
|
write_build_info "$(get_version)" "$OLD_REV"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "[i] Checkout auf Release ${LATEST_TAG} (${TARGET_REV:0:7}) …"
|
||||||
|
as_app "git -C ${APP_DIR} checkout -q ${LATEST_TAG}"
|
||||||
|
fi
|
||||||
|
NEW_REV="$(as_app "git -C ${APP_DIR} rev-parse HEAD")"
|
||||||
|
else
|
||||||
|
# Rolling: branch folgen
|
||||||
|
as_app "git -C ${APP_DIR} fetch --quiet origin ${BRANCH}"
|
||||||
|
BEHIND="$(as_app "git -C ${APP_DIR} rev-list --count HEAD..origin/${BRANCH} || echo 0")"
|
||||||
|
if [[ "$BEHIND" -eq 0 ]]; then
|
||||||
|
echo "[✓] Branch origin/${BRANCH} ist bereits aktuell – nichts zu tun."
|
||||||
|
write_build_info "$(get_version)" "$OLD_REV"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "[i] Es gibt ${BEHIND} neue Commit(s) – ziehe Änderungen …"
|
||||||
|
as_app "git -C ${APP_DIR} checkout -q ${BRANCH} && git -C ${APP_DIR} pull --ff-only origin ${BRANCH}"
|
||||||
|
NEW_REV="$(as_app "git -C ${APP_DIR} rev-parse HEAD")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -------- Änderungstypen ermitteln -------------------------------------------
|
||||||
|
CHANGED_FILES="$(as_app "git -C ${APP_DIR} diff --name-only ${OLD_REV}..${NEW_REV}")"
|
||||||
|
|
||||||
|
NEED_COMPOSER=0
|
||||||
|
NEED_MIGRATIONS=0
|
||||||
|
NEED_FRONTEND=0
|
||||||
|
NEED_PHP_RESTART=0
|
||||||
|
|
||||||
|
echo "$CHANGED_FILES" | grep -qE '(^|/)composer\.(json|lock)$' && NEED_COMPOSER=1
|
||||||
|
echo "$CHANGED_FILES" | grep -qE '^database/migrations/' && NEED_MIGRATIONS=1
|
||||||
|
echo "$CHANGED_FILES" | grep -qE '^(package(-lock)?\.json|vite\.config(\.ts|\.js)?|resources/|public/.*\.(js|css))' && NEED_FRONTEND=1
|
||||||
|
echo "$CHANGED_FILES" | grep -qE '^(app/|routes/|config/|resources/views/)' && NEED_PHP_RESTART=1
|
||||||
|
|
||||||
|
echo "[i] Zusammenfassung:"
|
||||||
|
echo " Composer : $([[ $NEED_COMPOSER -eq 1 ]] && echo JA || echo nein)"
|
||||||
|
echo " Migrations : $([[ $NEED_MIGRATIONS -eq 1 ]] && echo JA || echo nein)"
|
||||||
|
echo " Frontend : $([[ $NEED_FRONTEND -eq 1 ]] && echo JA || echo nein)"
|
||||||
|
echo " PHP restart : $([[ $NEED_PHP_RESTART -eq 1 ]] && echo JA || echo nein)"
|
||||||
|
|
||||||
|
# Wenn gar nichts relevantes geändert wurde → sauber beenden
|
||||||
|
if [[ $NEED_COMPOSER -eq 0 && $NEED_MIGRATIONS -eq 0 && $NEED_FRONTEND -eq 0 && $NEED_PHP_RESTART -eq 0 ]]; then
|
||||||
|
echo "[✓] Code-Stand aktualisiert, aber keine Build/Runtime-Änderungen – keine Neustarts nötig."
|
||||||
|
NEW_VER="$(get_version)"
|
||||||
|
write_build_info "$NEW_VER" "$NEW_REV"
|
||||||
|
echo "[i] Version: ${OLD_VER} → ${NEW_VER}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -------- Composer (ohne Downtime) -------------------------------------------
|
||||||
|
if [[ $NEED_COMPOSER -eq 1 ]]; then
|
||||||
|
echo "[i] Composer …"
|
||||||
|
as_app "cd ${APP_DIR} && composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -------- Frontend (ohne Downtime — Assets sind statisch) --------------------
|
||||||
|
if [[ $NEED_FRONTEND -eq 1 ]]; then
|
||||||
|
echo "[i] Frontend-Änderungen erkannt – baue Assets …"
|
||||||
|
if ! frontend_build_quiet; then
|
||||||
|
echo "[!] Frontend-Build ist fehlgeschlagen (siehe /var/log/mailwolt-frontend-build.log)."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -------- Wartungsmodus: Migrations + Cache + PHP-FPM reload -----------------
|
||||||
|
# Trap stellt sicher dass artisan up auch bei Fehler läuft
|
||||||
|
MAINTENANCE_ACTIVE=0
|
||||||
|
cleanup_maintenance(){
|
||||||
|
if [[ "$MAINTENANCE_ACTIVE" -eq 1 ]]; then
|
||||||
|
artisan_up
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup_maintenance EXIT INT TERM
|
||||||
|
|
||||||
|
if [[ $NEED_MIGRATIONS -eq 1 || $NEED_PHP_RESTART -eq 1 || $NEED_COMPOSER -eq 1 ]]; then
|
||||||
|
artisan_down
|
||||||
|
MAINTENANCE_ACTIVE=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $NEED_MIGRATIONS -eq 1 ]]; then
|
||||||
|
echo "[i] DB-Migrationen …"
|
||||||
|
as_app "cd ${APP_DIR} && php artisan migrate --force"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $NEED_PHP_RESTART -eq 1 || $NEED_COMPOSER -eq 1 || $NEED_MIGRATIONS -eq 1 ]]; then
|
||||||
|
echo "[i] Cache/Optimierungen …"
|
||||||
|
as_app "cd ${APP_DIR} && php artisan optimize:clear || true"
|
||||||
|
as_app "cd ${APP_DIR} && php artisan config:cache || true"
|
||||||
|
as_app "cd ${APP_DIR} && php artisan route:cache || true"
|
||||||
|
as_app "cd ${APP_DIR} && php artisan queue:restart || true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -------- Dienste neu laden (gezielt) ----------------------------------------
|
||||||
|
echo "[i] Dienste neu laden …"
|
||||||
|
if [[ $NEED_PHP_RESTART -eq 1 || $NEED_COMPOSER -eq 1 || $NEED_MIGRATIONS -eq 1 ]]; then
|
||||||
|
restart_php_fpm
|
||||||
|
restart_if_exists "${APP_USER}-queue"
|
||||||
|
restart_if_exists "${APP_USER}-schedule"
|
||||||
|
restart_if_exists "${APP_USER}-ws"
|
||||||
|
fi
|
||||||
|
if [[ $NEED_FRONTEND -eq 1 || $NEED_PHP_RESTART -eq 1 ]]; then
|
||||||
|
reload_if_active nginx
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -------- Wartungsmodus beenden ----------------------------------------------
|
||||||
|
if [[ "$MAINTENANCE_ACTIVE" -eq 1 ]]; then
|
||||||
|
artisan_up
|
||||||
|
MAINTENANCE_ACTIVE=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -------- Build-Info ablegen --------------------------------------------------
|
||||||
|
NEW_VER="$(get_version)"
|
||||||
|
write_build_info "$NEW_VER" "$NEW_REV"
|
||||||
|
|
||||||
|
echo "[✓] Update abgeschlossen: ${OLD_REV:0:7} → ${NEW_REV:0:7} (Version: ${NEW_VER})"
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MSG=""
|
||||||
|
BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo main)"
|
||||||
|
REMOTE="${REMOTE:-origin}"
|
||||||
|
|
||||||
|
usage(){ echo "Usage: $0 -m \"commit message\" [-b branch]"; exit 1; }
|
||||||
|
|
||||||
|
while getopts ":m:b:" opt; do
|
||||||
|
case "$opt" in
|
||||||
|
m) MSG="$OPTARG" ;;
|
||||||
|
b) BRANCH="$OPTARG" ;;
|
||||||
|
*) usage ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
shift $((OPTIND-1))
|
||||||
|
|
||||||
|
[[ -n "$MSG" ]] || usage
|
||||||
|
git rev-parse --git-dir >/dev/null 2>&1 || { echo "[x] kein Git-Repo"; exit 1; }
|
||||||
|
|
||||||
|
echo "[i] Add/Commit → $BRANCH"
|
||||||
|
git add -A
|
||||||
|
# Kein Commit, wenn nichts geändert ist
|
||||||
|
if ! git diff --cached --quiet; then
|
||||||
|
git commit -m "$MSG"
|
||||||
|
else
|
||||||
|
echo "[i] Nichts zu committen (Index leer) – pushe nur."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[i] Push → $REMOTE/$BRANCH"
|
||||||
|
git push "$REMOTE" "$BRANCH"
|
||||||
|
echo "[✓] Done."
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REMOTE="${REMOTE:-origin}"
|
||||||
|
BRANCH="${BRANCH:-main}"
|
||||||
|
ALLOW_DIRTY=0
|
||||||
|
VERSION=""
|
||||||
|
MSG=""
|
||||||
|
|
||||||
|
usage(){
|
||||||
|
echo "Usage: $0 <X.Y.Z> [-m \"release notes\"] [-b branch] [--allow-dirty]"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Args
|
||||||
|
[[ $# -ge 1 ]] || usage
|
||||||
|
VERSION="$1"; shift || true
|
||||||
|
[[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "[x] Ungültige Version: $VERSION (erwarte SemVer X.Y.Z)"; exit 1; }
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-m) MSG="$2"; shift 2 ;;
|
||||||
|
-b) BRANCH="$2"; shift 2 ;;
|
||||||
|
--allow-dirty) ALLOW_DIRTY=1; shift ;;
|
||||||
|
*) usage ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
git rev-parse --git-dir >/dev/null 2>&1 || { echo "[x] kein Git-Repo"; exit 1; }
|
||||||
|
|
||||||
|
# Clean tree?
|
||||||
|
if [[ $ALLOW_DIRTY -eq 0 ]]; then
|
||||||
|
if ! git diff --quiet || ! git diff --cached --quiet; then
|
||||||
|
echo "[x] Arbeitsbaum nicht sauber. Committe oder nutze --allow-dirty."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Aktuellen Stand holen
|
||||||
|
git fetch --quiet "$REMOTE" --tags
|
||||||
|
# Branch check-out/sync
|
||||||
|
git checkout -q "$BRANCH"
|
||||||
|
git pull --ff-only "$REMOTE" "$BRANCH"
|
||||||
|
|
||||||
|
TAG="v${VERSION}"
|
||||||
|
if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
|
||||||
|
echo "[x] Tag ${TAG} existiert bereits."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
LAST_TAG="$(git describe --tags --abbrev=0 2>/dev/null || true)"
|
||||||
|
if [[ -n "$LAST_TAG" ]]; then
|
||||||
|
CHANGELOG="$(git log --pretty=format:'- %s (%h)' "${LAST_TAG}..HEAD" || true)"
|
||||||
|
else
|
||||||
|
CHANGELOG="$(git log --pretty=format:'- %s (%h)' || true)"
|
||||||
|
fi
|
||||||
|
[[ -n "$CHANGELOG" ]] || CHANGELOG="- initial release content"
|
||||||
|
|
||||||
|
# VERSION bumpen
|
||||||
|
echo -n "$VERSION" > VERSION
|
||||||
|
git add VERSION
|
||||||
|
git commit -m "chore(release): v${VERSION}"
|
||||||
|
|
||||||
|
# Tag-Message bauen
|
||||||
|
if [[ -z "$MSG" ]]; then
|
||||||
|
read -r -d '' MSG <<EOF || true
|
||||||
|
MailWolt v${VERSION}
|
||||||
|
|
||||||
|
Changes since ${LAST_TAG:-repo start}:
|
||||||
|
${CHANGELOG}
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
git tag -a "$TAG" -m "$MSG"
|
||||||
|
|
||||||
|
# Push commit + tag
|
||||||
|
git push "$REMOTE" "$BRANCH"
|
||||||
|
git push "$REMOTE" "$TAG"
|
||||||
|
|
||||||
|
echo "[✓] Release ${TAG} erstellt & gepusht."
|
||||||
|
|
@ -163,7 +163,9 @@
|
||||||
|
|
||||||
{{-- ── Schritt 5: Domain-Setup ── --}}
|
{{-- ── Schritt 5: Domain-Setup ── --}}
|
||||||
@elseif($step === 5)
|
@elseif($step === 5)
|
||||||
<div wire:poll.2s="pollSetup"></div>
|
@if(!$setupDone)
|
||||||
|
<div wire:poll.2s="pollSetup"></div>
|
||||||
|
@endif
|
||||||
|
|
||||||
@php
|
@php
|
||||||
$anyFailed = collect($domainStatus)->contains(fn($s) => in_array($s, ['error','nodns','noipv6']));
|
$anyFailed = collect($domainStatus)->contains(fn($s) => in_array($s, ['error','nodns','noipv6']));
|
||||||
|
|
@ -292,12 +294,10 @@
|
||||||
</div>
|
</div>
|
||||||
@elseif($step === 5 && $setupDone)
|
@elseif($step === 5 && $setupDone)
|
||||||
<div style="display:flex;justify-content:flex-end;margin-top:20px">
|
<div style="display:flex;justify-content:flex-end;margin-top:20px">
|
||||||
{{-- Kein wire:click — plain Link damit kein Livewire-POST nötig ist.
|
<button wire:click="goToLogin" class="mbx-btn-primary" style="font-size:12.5px;width:fit-content">
|
||||||
nginx leitet /login nach SSL-Switch automatisch auf HTTPS weiter. --}}
|
|
||||||
<a href="/login" class="mbx-btn-primary" style="font-size:12.5px;width:fit-content;text-decoration:none;display:inline-flex;align-items:center;gap:6px">
|
|
||||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M2 6.5l2.5 2.5 5.5-5.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M2 6.5l2.5 2.5 5.5-5.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||||
{{ collect($domainStatus)->contains(fn($s) => in_array($s, ['error','nodns','noipv6'])) ? 'Trotzdem zum Login' : 'Zum Login' }}
|
{{ collect($domainStatus)->contains(fn($s) => in_array($s, ['error','nodns','noipv6'])) ? 'Trotzdem zum Login' : 'Zum Login' }}
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue