#!/bin/bash # archivmail Installer # Unterstütztes System: Debian 13 (trixie) # # Aufruf: # bash install.sh # Interaktiv (Modus-Auswahl) # INSTALL_MODE=native bash install.sh # Nativ (systemd, direkter Build) # INSTALL_MODE=docker bash install.sh # Docker (alle Abhängigkeiten im Container) set -euo pipefail export DEBIAN_FRONTEND=noninteractive RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m' log() { echo -e "${GREEN}[OK]${NC} $*"; } info() { echo -e "${BLUE}[..]${NC} $*"; } warn() { echo -e "${YELLOW}[!!]${NC} $*"; } die() { echo -e "${RED}[ERR]${NC} $*" >&2; exit 1; } # PROJ-68: Provisioniert sudo-NOPASSWD-Rechte für die Admin-Dienststeuerung # (Start/Stop/Restart/Enable/Disable über den "Dienste"-Tab). Muss mit # `allowedServices` in internal/api/admin_services_handlers.go synchron # gehalten werden. Installiert `sudo` falls fehlend, validiert die generierte # Datei per `visudo -c` VOR dem Einspielen — eine kaputte sudoers-Datei würde # sonst den gesamten sudo-Mechanismus auf dem Server lahmlegen. Idempotent: # überschreibt die Datei bei jedem Lauf mit demselben, deterministischen # Inhalt (kein Diff-Merge nötig, da wir hier die einzige Quelle dieser Regeln # sind — ein manuell abweichend konfiguriertes System würde überschrieben, # das ist so beabsichtigt, damit Code und Server nie auseinanderlaufen). provision_archivmail_sudoers() { local am_user="${1:-$AM_USER}" command -v sudo >/dev/null 2>&1 || apt-get install -y -qq sudo local services=("archivmail" "archivmail-web" "manticore" "postgresql@17-main" "postfix" "nginx") local tmpfile tmpfile="$(mktemp)" { echo "# Verwaltet von install.sh/update.sh (PROJ-68) — nicht manuell editieren," echo "# Änderungen gehen beim nächsten Deploy verloren. Whitelist muss mit" echo "# allowedServices in internal/api/admin_services_handlers.go übereinstimmen." for svc in "${services[@]}"; do echo "${am_user} ALL=(root) NOPASSWD: /usr/bin/systemctl start ${svc}.service" echo "${am_user} ALL=(root) NOPASSWD: /usr/bin/systemctl stop ${svc}.service" echo "${am_user} ALL=(root) NOPASSWD: /usr/bin/systemctl restart ${svc}.service" echo "${am_user} ALL=(root) NOPASSWD: /usr/bin/systemctl enable ${svc}.service" echo "${am_user} ALL=(root) NOPASSWD: /usr/bin/systemctl disable ${svc}.service" done } > "$tmpfile" if visudo -c -f "$tmpfile" >/dev/null 2>&1; then install -m 0440 -o root -g root "$tmpfile" /etc/sudoers.d/archivmail log "sudo-Rechte für Dienststeuerung eingerichtet (/etc/sudoers.d/archivmail)" else warn "Generierte sudoers-Regel ist ungültig — /etc/sudoers.d/archivmail NICHT verändert" fi rm -f "$tmpfile" } # PROJ-66: Ein Backup (App-Backup oder PBS/Snapshot) ist ohne dieses Keyfile # wertlos — der Store bleibt Chiffretext. Der Inhalt wird EINMALIG bei der # Generierung ausgegeben, damit er sofort in einen Passwort-Safe (z.B. # Bitwarden Secure Note, siehe features/PROJ-66-backup-strategie.md) verschoben # werden kann, statt sich später darauf zu verlassen, dass irgendein Backup # das Keyfile schon mitgesichert hat. print_keyfile_escrow_warning() { local keyfile_path="$1" echo warn "WICHTIG — Keyfile jetzt sichern (Restore ist ohne dieses Keyfile unmöglich):" echo -e "${YELLOW}$(cat "$keyfile_path")${NC}" warn "Diesen Wert in einen separaten Passwort-Safe kopieren (z.B. Bitwarden Secure" warn "Note, eigene Collection mit eingeschränktem Zugriff) — unabhängig von jedem" warn "Server-Backup. Ohne Keyfile sind gesicherte Mail-Dateien nicht entschlüsselbar." echo } [[ $EUID -eq 0 ]] || die "Bitte als root ausführen: sudo bash install.sh" # ── OS-Prüfung: nur Debian 13 (trixie) ────────────────────────────────────── _os_id=$(. /etc/os-release 2>/dev/null && echo "$ID" || echo "") _os_ver=$(. /etc/os-release 2>/dev/null && echo "$VERSION_ID" || echo "") [[ "$_os_id" == "debian" && "$_os_ver" == "13" ]] \ || die "Nur Debian 13 (trixie) wird unterstützt (erkannt: ${_os_id} ${_os_ver})" log "Debian 13 erkannt" # ── Gemeinsame Variablen ────────────────────────────────────────────────────── DB_PASSWORD="${DB_PASSWORD:-$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 32)}" API_SECRET="${API_SECRET:-$(openssl rand -base64 48 | tr -dc 'a-zA-Z0-9' | head -c 64)}" WEBHOOK_SECRET="${WEBHOOK_SECRET:-$(openssl rand -base64 32 | tr -dc 'a-zA-Z0-9' | head -c 40)}" INSTALL_DIR="/opt/archivmail" STORE_DIR="/var/archivmail" LOG_DIR="/var/log/archivmail" CONFIG_DIR="/etc/archivmail" SSL_DIR="/etc/ssl/archivmail" AM_USER="archivmail" FQDN="$(hostname -f 2>/dev/null || hostname)" REPO_URL="${REPO_URL:-https://gitea.perlbach24.de/scripte/archivmail.git}" # ── Banner ──────────────────────────────────────────────────────────────────── echo "" echo " ╔══════════════════════════════════════════╗" echo " ║ archivmail Installer v2.0 ║" echo " ╚══════════════════════════════════════════╝" echo "" info "Hostname: $FQDN" echo "" # ── Modus-Auswahl ───────────────────────────────────────────────────────────── INSTALL_MODE="${INSTALL_MODE:-}" if [[ -z "$INSTALL_MODE" ]]; then echo " Installations-Modus wählen:" echo "" echo " 1) Nativ — Go/Node.js direkt auf dem Server kompiliert und gebaut" echo " (systemd-Dienste, PostgreSQL lokal, kein Docker nötig)" echo "" echo " 2) Docker — Alle Abhängigkeiten (Go, Node.js, PostgreSQL, Manticore)" echo " laufen in Containern. Kein Build-Toolchain auf dem Host." echo " Deployment via GitHub Webhook (git pull → docker compose up)" echo "" read -rp " Auswahl [1/2]: " _choice case "$_choice" in 1) INSTALL_MODE="native" ;; 2) INSTALL_MODE="docker" ;; *) die "Ungültige Auswahl: $_choice" ;; esac fi [[ "$INSTALL_MODE" == "native" || "$INSTALL_MODE" == "docker" ]] \ || die "INSTALL_MODE muss 'native' oder 'docker' sein (aktuell: $INSTALL_MODE)" echo "" info "Modus: $INSTALL_MODE" echo "" # ══════════════════════════════════════════════════════════════════════════════ # DOCKER-INSTALLATION # ══════════════════════════════════════════════════════════════════════════════ install_docker() { # ── Docker installieren ─────────────────────────────────────────────────── info "Installiere Docker..." if command -v docker &>/dev/null; then log "Docker bereits installiert: $(docker --version)" else apt-get update -qq apt-get install -y -qq ca-certificates curl gnupg install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/debian/gpg \ | gpg --dearmor -o /etc/apt/keyrings/docker.gpg chmod a+r /etc/apt/keyrings/docker.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \ > /etc/apt/sources.list.d/docker.list apt-get update -qq apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-compose-plugin systemctl enable --now docker log "Docker $(docker --version) installiert" fi # ── Hilfspakete ─────────────────────────────────────────────────────────── info "Installiere Hilfspakete..." apt-get install -y -qq git curl nginx openssl logrotate log "Pakete installiert" # ── webhook-Binary (adnanh/webhook) ────────────────────────────────────── info "Installiere webhook-Empfänger..." if ! command -v webhook &>/dev/null; then WEBHOOK_BIN_VERSION="2.8.2" _arch="$(dpkg --print-architecture)" [[ "$_arch" == "amd64" ]] && _arch="linux_amd64" [[ "$_arch" == "arm64" ]] && _arch="linux_arm64" curl -fsSL \ "https://github.com/adnanh/webhook/releases/download/${WEBHOOK_BIN_VERSION}/webhook-${_arch}.tar.gz" \ | tar -xz -C /usr/local/bin --strip-components=1 "webhook-${_arch}/webhook" chmod +x /usr/local/bin/webhook log "webhook ${WEBHOOK_BIN_VERSION} installiert" else log "webhook bereits installiert" fi # ── Verzeichnisse ───────────────────────────────────────────────────────── info "Erstelle Verzeichnisse..." mkdir -p "$CONFIG_DIR" "$LOG_DIR" log "Verzeichnisse erstellt" # ── Repository klonen ───────────────────────────────────────────────────── info "Klone Repository..." if [[ -d "$INSTALL_DIR/.git" ]]; then log "Repository existiert bereits — git pull" git -C "$INSTALL_DIR" pull origin main else git clone "$REPO_URL" "$INSTALL_DIR" log "Repository geklont: $INSTALL_DIR" fi # ── Keyfile ─────────────────────────────────────────────────────────────── info "Generiere Verschlüsselungs-Keyfile..." if [[ ! -f "$CONFIG_DIR/keyfile" ]]; then openssl rand -base64 32 > "$CONFIG_DIR/keyfile" chmod 400 "$CONFIG_DIR/keyfile" log "Keyfile generiert: $CONFIG_DIR/keyfile" print_keyfile_escrow_warning "$CONFIG_DIR/keyfile" else log "Keyfile existiert bereits – wird nicht überschrieben" fi # ── TLS-Zertifikat ──────────────────────────────────────────────────────── info "Erstelle selbstsigniertes TLS-Zertifikat..." mkdir -p "$SSL_DIR" if [[ ! -f "$SSL_DIR/archivmail.crt" ]]; then SERVER_IP="$(hostname -I | awk '{print $1}')" openssl req -x509 -nodes -days 3650 -newkey rsa:4096 \ -keyout "$SSL_DIR/archivmail.key" \ -out "$SSL_DIR/archivmail.crt" \ -subj "/CN=${FQDN}/O=archivmail/C=DE" \ -addext "subjectAltName=DNS:${FQDN},DNS:$(hostname -s),IP:${SERVER_IP}" \ 2>/dev/null chmod 640 "$SSL_DIR/archivmail.key" chmod 644 "$SSL_DIR/archivmail.crt" log "TLS-Zertifikat erstellt" else log "TLS-Zertifikat existiert bereits – wird nicht überschrieben" fi # ── config.yml aus Vorlage ──────────────────────────────────────────────── info "Erstelle Konfigurationsdatei..." if [[ ! -f "$CONFIG_DIR/config.yml" ]]; then sed \ -e "s/CHANGE_ME_DB_PASSWORD/$DB_PASSWORD/g" \ -e "s/CHANGE_ME_64_CHAR_SECRET/$API_SECRET/g" \ -e "s/mail\.example\.com/$FQDN/g" \ "$INSTALL_DIR/config/config.docker.yml.example" \ > "$CONFIG_DIR/config.yml" chmod 640 "$CONFIG_DIR/config.yml" log "config.yml erstellt: $CONFIG_DIR/config.yml" else log "config.yml existiert bereits – wird nicht überschrieben" fi # ── .env für docker-compose ─────────────────────────────────────────────── info "Erstelle .env..." if [[ ! -f "$INSTALL_DIR/.env" ]]; then printf 'DB_PASSWORD=%s\n' "$DB_PASSWORD" > "$INSTALL_DIR/.env" chmod 600 "$INSTALL_DIR/.env" log ".env erstellt" else log ".env existiert bereits – wird nicht überschrieben" fi # ── deploy.sh ausführbar ────────────────────────────────────────────────── chmod +x "$INSTALL_DIR/scripts/deploy.sh" # ── logrotate ───────────────────────────────────────────────────────────── cat > /etc/logrotate.d/archivmail < /etc/systemd/system/archivmail-webhook.service < /etc/nginx/sites-available/archivmail < "$summary_file" </dev/null | grep -q "go${GO_VERSION}"; then log "Go ${GO_VERSION} bereits installiert" else _go_arch="amd64" [[ "$(dpkg --print-architecture)" == "arm64" ]] && _go_arch="arm64" curl -fsSL "https://dl.google.com/go/go${GO_VERSION}.linux-${_go_arch}.tar.gz" \ | tar -C /usr/local -xz log "Go $(/usr/local/go/bin/go version | awk '{print $3}') installiert" fi export PATH="$PATH:/usr/local/go/bin" ln -sf /usr/local/go/bin/go /usr/local/bin/go ln -sf /usr/local/go/bin/gofmt /usr/local/bin/gofmt # ── 1c. Manticore Search ────────────────────────────────────────────────── # Kein natives trixie-Repo (siehe update.sh für Details) — bookworm-Paket # ist lauffähig. Seit ~v25 ein einziges "manticore"-Paket statt der # früheren manticoresearch_*-Aufteilung; GitHub-Releases liefern keine # .deb-Assets mehr, daher kein Fallback mehr dorthin. Version wird dynamisch # aus der Packages-Datei des Repos gelesen (letzter "Package: manticore"- # Block); _MANTICORE_VERSION_FALLBACK greift nur, wenn die Packages-Datei # nicht erreichbar/parsbar ist. _MANTICORE_VERSION_FALLBACK="27.1.5-26061911-5a1cf9399" _mc_installed_version="$(dpkg-query -W -f='${Version}' manticore 2>/dev/null || true)" _mc_arch="$(dpkg --print-architecture)" _mc_latest_from_repo() { local base="https://repo.manticoresearch.com/repository/manticoresearch_bookworm" local packages block version filename packages="$(curl -fsSL --max-time 15 "${base}/dists/bookworm/main/binary-${_mc_arch}/Packages" 2>/dev/null)" || return 1 [[ -n "$packages" ]] || return 1 block="$(printf '%s\n' "$packages" | awk 'BEGIN{RS="\n\n"} {n=split($0,l,"\n"); if (l[1]=="Package: manticore") b=$0} END{print b}')" [[ -n "$block" ]] || return 1 version="$(printf '%s\n' "$block" | awk -F': ' '/^Version:/{print $2; exit}')" filename="$(printf '%s\n' "$block" | awk -F': ' '/^Filename:/{print $2; exit}')" [[ -n "$version" && -n "$filename" ]] || return 1 printf '%s|%s\n' "$version" "$filename" } _mc_target_version="$_MANTICORE_VERSION_FALLBACK" _mc_target_url="https://repo.manticoresearch.com/repository/manticoresearch_bookworm/dists/bookworm/main/binary-${_mc_arch}/manticore_${_MANTICORE_VERSION_FALLBACK}_${_mc_arch}.deb" if _mc_latest="$(_mc_latest_from_repo)"; then _mc_target_version="${_mc_latest%%|*}" _mc_target_url="https://repo.manticoresearch.com/repository/manticoresearch_bookworm/${_mc_latest#*|}" info "Manticore Search: aktuelle Repo-Version ${_mc_target_version} ermittelt" else warn "Manticore-Repo-Metadaten nicht abrufbar — falle auf bekannten Stand ${_MANTICORE_VERSION_FALLBACK} zurück" fi _mc_fetch_and_install() { wget -q -O /tmp/manticore.deb "$_mc_target_url" || true if [[ ! -f /tmp/manticore.deb ]]; then return 1 fi dpkg -i /tmp/manticore.deb 2>/dev/null || apt-get install -f -y -qq 2>/dev/null || true rm -f /tmp/manticore.deb return 0 } if [[ -z "$_mc_installed_version" ]]; then info "Installiere Manticore Search ${_mc_target_version}..." _mc_fetch_and_install && log "Manticore Search installiert" \ || die "Manticore Search konnte nicht installiert werden — siehe: https://manticoresearch.com/install/" elif dpkg --compare-versions "$_mc_installed_version" lt "$_mc_target_version"; then # PROJ-67: Upgrade statt Skip, damit auch Bestandsinstallationen beim # nächsten install.sh-/update.sh-Lauf mitgezogen werden. "lt"-Vergleich # schützt gegen versehentliches Downgrade. info "Manticore Search ${_mc_installed_version} gefunden, aktualisiere auf ${_mc_target_version}..." systemctl stop manticore 2>/dev/null || true _mc_fetch_and_install \ && log "Manticore Search aktualisiert: ${_mc_installed_version} → ${_mc_target_version}" \ || warn "Upgrade fehlgeschlagen — vorherige Version bleibt installiert" else log "Manticore Search ${_mc_installed_version} bereits aktuell (>= ${_mc_target_version})" fi systemctl enable --now manticore 2>/dev/null || warn "Manticore-Dienst konnte nicht gestartet werden" systemctl is-active --quiet manticore && log "Manticore Search läuft" # ── 2. Systembenutzer ───────────────────────────────────────────────────── info "Lege Systembenutzer '$AM_USER' an..." id "$AM_USER" &>/dev/null \ && log "Benutzer '$AM_USER' existiert bereits" \ || { useradd --system --shell /bin/false --home "$STORE_DIR" --create-home "$AM_USER"; log "Benutzer angelegt"; } # ── 2b. sudo-Rechte für die Admin-Dienststeuerung (PROJ-68) ──────────────── # internal/api/admin_services_handlers.go ruft für Start/Stop/Restart/ # Enable/Disable IMMER `sudo systemctl ...` auf — ohne diese Provisionierung # schlägt jede Dienst-Aktion aus dem Admin-"Dienste"-Tab mit einem # schwer diagnostizierbaren HTTP 500 fehl (sudo: command not found). # Whitelist MUSS mit `allowedServices` in admin_services_handlers.go # synchron bleiben. provision_archivmail_sudoers # ── 3. Verzeichnisstruktur ──────────────────────────────────────────────── info "Erstelle Verzeichnisstruktur..." mkdir -p "$STORE_DIR/store" "$STORE_DIR/astore" mkdir -p "$CONFIG_DIR" "$LOG_DIR" "$INSTALL_DIR" "$INSTALL_DIR/web" "$SSL_DIR" chown -R "$AM_USER:$AM_USER" "$STORE_DIR" "$LOG_DIR" chmod 755 "$STORE_DIR" chmod 700 "$STORE_DIR/store" "$STORE_DIR/astore" log "Verzeichnisse erstellt" # ── 4. Keyfile ──────────────────────────────────────────────────────────── info "Generiere Verschlüsselungs-Keyfile..." if [ ! -f "$CONFIG_DIR/keyfile" ]; then openssl rand -base64 32 > "$CONFIG_DIR/keyfile" chmod 400 "$CONFIG_DIR/keyfile" chown "$AM_USER:$AM_USER" "$CONFIG_DIR/keyfile" log "Keyfile generiert: $CONFIG_DIR/keyfile" print_keyfile_escrow_warning "$CONFIG_DIR/keyfile" else log "Keyfile existiert bereits – wird nicht überschrieben" fi # ── 5. TLS-Zertifikat ───────────────────────────────────────────────────── info "Erstelle selbstsigniertes TLS-Zertifikat..." if [ ! -f "$SSL_DIR/archivmail.crt" ]; then SERVER_IP="$(hostname -I | awk '{print $1}')" openssl req -x509 -nodes -days 3650 -newkey rsa:4096 \ -keyout "$SSL_DIR/archivmail.key" \ -out "$SSL_DIR/archivmail.crt" \ -subj "/CN=${FQDN}/O=archivmail/C=DE" \ -addext "subjectAltName=DNS:${FQDN},DNS:$(hostname -s),IP:${SERVER_IP}" \ 2>/dev/null chmod 640 "$SSL_DIR/archivmail.key" chmod 644 "$SSL_DIR/archivmail.crt" chown "root:$AM_USER" "$SSL_DIR/archivmail.key" log "TLS-Zertifikat erstellt: $SSL_DIR/archivmail.crt (FQDN: $FQDN, IP: $SERVER_IP)" else log "TLS-Zertifikat existiert bereits – wird nicht überschrieben" fi # ── 6. PostgreSQL ───────────────────────────────────────────────────────── info "Richte PostgreSQL ein..." systemctl enable postgresql --quiet systemctl start postgresql # Falls config.yml schon existiert, DB-Passwort daraus lesen (verhindert Passwort-Mismatch bei Re-Install) if [ -f "$CONFIG_DIR/config.yml" ]; then EXISTING_PW=$(grep -A5 '^database:' "$CONFIG_DIR/config.yml" | awk '/password:/{print $2}' | head -1) [[ -n "$EXISTING_PW" ]] && DB_PASSWORD="$EXISTING_PW" && info "DB-Passwort aus vorhandener config.yml übernommen" fi su -c "psql -tc \"SELECT 1 FROM pg_roles WHERE rolname='archivmail'\" | grep -q 1 \ && psql -c \"ALTER USER archivmail WITH PASSWORD '$DB_PASSWORD'\" \ || psql -c \"CREATE USER archivmail WITH PASSWORD '$DB_PASSWORD'\"" postgres su -c "psql -tc \"SELECT 1 FROM pg_database WHERE datname='archivmail'\" | grep -q 1 || \ psql -c \"CREATE DATABASE archivmail OWNER archivmail\"" postgres su -c "psql archivmail -c \"GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO archivmail;\"" postgres su -c "psql archivmail -c \"GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO archivmail;\"" postgres su -c "psql archivmail -c \"ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO archivmail;\"" postgres su -c "psql archivmail -c \"ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO archivmail;\"" postgres log "PostgreSQL eingerichtet" # ── 7. Konfiguration ────────────────────────────────────────────────────── info "Erstelle Konfigurationsdatei..." if [ ! -f "$CONFIG_DIR/config.yml" ]; then cat > "$CONFIG_DIR/config.yml" << CONFIG # archivmail Konfiguration – generiert am $(date -u +%Y-%m-%dT%H:%M:%SZ) server: api_port: 8080 smtp_port: 2525 database: host: 127.0.0.1 port: 5432 name: archivmail user: archivmail password: ${DB_PASSWORD} sslmode: disable storage: store_path: ${STORE_DIR}/store astore_path: ${STORE_DIR}/astore keyfile: ${CONFIG_DIR}/keyfile api: bind: ":8080" secret: ${API_SECRET} trusted_proxies: - 127.0.0.1 secure_cookies: true index: backend: manticore manticore_dsn: "manticore@tcp(127.0.0.1:9306)/" batch_size: 100 batch_mode: true ocr: batch_mode: true audit: log_path: ${LOG_DIR}/audit.log retention_days: 0 smtp: enabled: true bind: ":2525" domain: "${FQDN}" tls_cert: ${SSL_DIR}/archivmail.crt tls_key: ${SSL_DIR}/archivmail.key allowed_ips: - 127.0.0.1 imap_server: enabled: true bind: ":993" tls_cert: ${SSL_DIR}/archivmail.crt tls_key: ${SSL_DIR}/archivmail.key CONFIG chmod 640 "$CONFIG_DIR/config.yml" chown "root:$AM_USER" "$CONFIG_DIR/config.yml" log "Konfiguration erstellt: $CONFIG_DIR/config.yml" else log "config.yml existiert bereits – wird nicht überschrieben" fi # ── 8. Nginx ────────────────────────────────────────────────────────────── info "Konfiguriere nginx (HTTP → HTTPS + TLS)..." cat > /etc/nginx/sites-available/archivmail << NGINX # HTTP → HTTPS Redirect server { listen 80; server_name ${FQDN} _; return 301 https://\$host\$request_uri; } # HTTPS server { listen 443 ssl; http2 on; server_name ${FQDN} _; ssl_certificate ${SSL_DIR}/archivmail.crt; ssl_certificate_key ${SSL_DIR}/archivmail.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; add_header Strict-Transport-Security "max-age=31536000" always; add_header X-Content-Type-Options nosniff always; add_header X-Frame-Options SAMEORIGIN always; location /api/ { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; client_max_body_size 512M; } location / { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; proxy_cache_bypass \$http_upgrade; } access_log /var/log/nginx/archivmail.access.log; error_log /var/log/nginx/archivmail.error.log; } NGINX ln -sf /etc/nginx/sites-available/archivmail /etc/nginx/sites-enabled/archivmail rm -f /etc/nginx/sites-enabled/default nginx -t systemctl enable nginx --quiet systemctl restart nginx log "nginx konfiguriert" # ── 9. logrotate ────────────────────────────────────────────────────────── cat > /etc/logrotate.d/archivmail << LOGROTATE ${LOG_DIR}/audit.log { daily rotate 365 compress delaycompress missingok notifempty create 640 ${AM_USER} ${AM_USER} } LOGROTATE log "logrotate konfiguriert" # ── 10. systemd Units ───────────────────────────────────────────────────── info "Erstelle systemd Units..." cat > /etc/systemd/system/archivmail.service << UNIT [Unit] Description=archivmail Mail Archive Daemon After=network.target postgresql.service manticore.service Requires=postgresql.service manticore.service [Service] Type=simple User=${AM_USER} Group=${AM_USER} # CAP_NET_ADMIN: required for the admin "enable firewall" action (nft -f /etc/nftables.conf) AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_NET_ADMIN # PROJ-68: CAP_SETUID/CAP_SETGID are NOT ambient (the Go daemon itself must # never hold them) but must be in the bounding set so the setuid-root sudo # child process (spawned by the admin "Dienste"-Tab for systemctl actions) # can actually change to uid/gid 0. Without this, sudo aborts with # "unable to change to root gid: Operation not permitted" even though the # sudoers NOPASSWD rule is correct — verified 2026-07-05, see # features/PROJ-68-sudo-provisionierung-dienststeuerung.md. CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_NET_ADMIN CAP_SETUID CAP_SETGID ExecStart=${INSTALL_DIR}/archivmail --config ${CONFIG_DIR}/config.yml ExecReload=/bin/kill -HUP \$MAINPID Restart=on-failure RestartSec=5 StandardOutput=journal StandardError=journal SyslogIdentifier=archivmail NoNewPrivileges=false ProtectSystem=strict ReadWritePaths=${STORE_DIR} ${LOG_DIR} ReadOnlyPaths=${CONFIG_DIR} ${SSL_DIR} [Install] WantedBy=multi-user.target UNIT cat > /etc/systemd/system/archivmail-web.service << UNIT [Unit] Description=archivmail Web Frontend After=network.target archivmail.service [Service] Type=simple User=${AM_USER} Group=${AM_USER} WorkingDirectory=${INSTALL_DIR}/web ExecStart=/usr/bin/node server.js Environment=NODE_ENV=production Environment=PORT=3000 Restart=on-failure RestartSec=5 StandardOutput=journal StandardError=journal SyslogIdentifier=archivmail-web [Install] WantedBy=multi-user.target UNIT systemctl daemon-reload systemctl enable archivmail archivmail-web --quiet log "systemd Units erstellt und aktiviert" # ── 11. Erstes Deployment via update.sh ─────────────────────────────────── info "Installiere update.sh und führe erstes Deployment durch..." _update_url="${REPO_URL/\.git/}/raw/branch/main/update.sh" curl -fsSL "$_update_url" -o "$INSTALL_DIR/update.sh" 2>/dev/null \ || { warn "update.sh konnte nicht geladen werden – überspringe Deployment"; } if [ -f "$INSTALL_DIR/update.sh" ]; then chmod +x "$INSTALL_DIR/update.sh" log "update.sh installiert" info "Führe erstes Deployment durch..." bash "$INSTALL_DIR/update.sh" for _i in $(seq 1 15); do _pw=$(journalctl -u archivmail --no-pager -n 100 | grep 'admin' | grep -oP ':\s+\K[0-9a-f]{16,}' | tail -1) [[ -n "$_pw" ]] && break sleep 1 done PW_SUPERADMIN=$(journalctl -u archivmail --no-pager -n 100 | grep 'superadmin' | grep -oP ':\s+\K\S+' | tail -1) PW_ADMIN=$(journalctl -u archivmail --no-pager -n 100 | grep ' admin ' | grep -v superadmin | grep -oP ':\s+\K\S+' | tail -1) PW_AUDITOR=$(journalctl -u archivmail --no-pager -n 100 | grep 'auditor' | grep -oP ':\s+\K\S+' | tail -1) [[ -n "$PW_ADMIN" ]] || PW_ADMIN="(nicht gefunden — journalctl -u archivmail | grep admin)" [[ -n "$PW_AUDITOR" ]] || PW_AUDITOR="(nicht gefunden — journalctl -u archivmail | grep auditor)" [[ -n "$PW_SUPERADMIN" ]] || PW_SUPERADMIN="(nicht gefunden — journalctl -u archivmail | grep superadmin)" else warn "update.sh fehlt — manuell: bash $INSTALL_DIR/update.sh" PW_SUPERADMIN="(noch nicht generiert)"; PW_ADMIN="(noch nicht generiert)"; PW_AUDITOR="(noch nicht generiert)" fi # ── Zusammenfassung ─────────────────────────────────────────────────────── SUMMARY_FILE="$CONFIG_DIR/install-summary.txt" cat > "$SUMMARY_FILE" << SUMMARY archivmail Native-Installation — $(date '+%d.%m.%Y %H:%M:%S') Server: $FQDN === ZUGANGSDATEN === Datenbank: archivmail / $DB_PASSWORD API-Secret: $API_SECRET Web-Logins (UNBEDINGT ÄNDERN!): superadmin@archivmail / $PW_SUPERADMIN admin@archivmail / $PW_ADMIN auditor@archivmail / $PW_AUDITOR === DIENSTE === Web (HTTPS): https://$FQDN IMAP (TLS): $FQDN:993 SMTP: $FQDN:2525 === DATEIPFADE === Konfiguration: $CONFIG_DIR/config.yml Keyfile: $CONFIG_DIR/keyfile TLS-Zertifikat: $SSL_DIR/archivmail.crt Mail-Speicher: $STORE_DIR/ Logs: $LOG_DIR/ Updater: $INSTALL_DIR/update.sh SUMMARY chmod 600 "$SUMMARY_FILE" log "Zusammenfassung: $SUMMARY_FILE" # ── Abschluss ───────────────────────────────────────────────────────────── echo "" echo " ╔══════════════════════════════════════════════════════════╗" echo " ║ Installation abgeschlossen! ║" echo " ╚══════════════════════════════════════════════════════════╝" echo "" echo " Web (HTTPS): https://$FQDN" echo " IMAP (TLS): $FQDN:993" echo " SMTP: $FQDN:2525" echo "" echo " ┌─────────────────────────────────────────────────────────┐" echo " │ ANGELEGTE BENUTZER & PASSWÖRTER │" echo " ├─────────────────────────────────────────────────────────┤" printf " │ DB archivmail: %-38s │\n" "$DB_PASSWORD" echo " ├─────────────────────────────────────────────────────────┤" echo " │ Web-Logins (UNBEDINGT ÄNDERN!): │" printf " │ superadmin / %-38s│\n" "$PW_SUPERADMIN" printf " │ admin / %-38s│\n" "$PW_ADMIN" printf " │ auditor / %-38s│\n" "$PW_AUDITOR" echo " └─────────────────────────────────────────────────────────┘" echo "" warn "Zusammenfassung mit Passwörtern: $SUMMARY_FILE" warn "Standardpasswörter unbedingt nach dem ersten Login ändern!" echo "" } # ══════════════════════════════════════════════════════════════════════════════ # MODUS AUSFÜHREN # ══════════════════════════════════════════════════════════════════════════════ case "$INSTALL_MODE" in docker) install_docker ;; native) install_native ;; esac