From 5f0c7a7e6d022523ca92b4bdb5196c10678b8b6c Mon Sep 17 00:00:00 2001 From: sysops Date: Tue, 31 Mar 2026 10:18:11 +0200 Subject: [PATCH] feat(PROJ-27): Docker-Support + install.sh v2.0 (native + Docker-Modus) Adds multi-stage Dockerfiles (Go+Xapian CGO, Next.js standalone), docker-compose.yml, webhook-basierter Deploy-Flow und erweitertes install.sh mit interaktiver Modus-Auswahl. Co-Authored-By: Claude Sonnet 4.6 --- .dockerignore | 30 ++ Dockerfile | 37 ++ Dockerfile.web | 31 ++ config/config.docker.yml.example | 57 +++ docker-compose.yml | 54 +++ features/INDEX.md | 8 +- install.sh | 758 ++++++++++++++++++++++--------- scripts/deploy.sh | 31 ++ scripts/webhook-hooks.json | 32 ++ 9 files changed, 814 insertions(+), 224 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 Dockerfile.web create mode 100644 config/config.docker.yml.example create mode 100644 docker-compose.yml create mode 100755 scripts/deploy.sh create mode 100644 scripts/webhook-hooks.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7dba3e4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,30 @@ +# Git +.git +.gitignore + +# Node +node_modules +.next +npm-debug.log* + +# Go build artifacts +archivmail +archivmail-export +archivmail-import + +# Dev/Test +*.test +config.test.yml +.env.local +.env.local.example + +# Secrets (nie ins Image) +keyfile +*.key +*.pem + +# Docs / IDE +docs/ +.claude/ +.vscode/ +*.md diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..08d3936 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,37 @@ +# ── Stage 1: Go Backend Build (CGO + Xapian) ───────────────────────────────── +FROM golang:1.24-bookworm AS go-builder + +RUN apt-get update && apt-get install -y --no-install-recommends \ + libxapian-dev pkg-config build-essential \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /build +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . +RUN CGO_ENABLED=1 go build -tags xapian -buildvcs=false \ + -ldflags="-s -w" \ + -o /out/archivmail ./cmd/archivmail/ + +# ── Stage 2: Runtime Image ──────────────────────────────────────────────────── +FROM debian:bookworm-slim + +# Xapian runtime library only (no dev headers) +RUN apt-get update && apt-get install -y --no-install-recommends \ + libxapian30 ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +RUN useradd --system --shell /bin/false --uid 999 --create-home archivmail + +COPY --from=go-builder /out/archivmail /usr/local/bin/archivmail + +# Volumes: config + data werden von außen gemountet +RUN mkdir -p /var/archivmail/store /var/archivmail/astore /var/archivmail/xapian \ + && chown -R archivmail:archivmail /var/archivmail + +USER archivmail +EXPOSE 8080 2525 1143 + +ENTRYPOINT ["/usr/local/bin/archivmail"] +CMD ["-config", "/etc/archivmail/config.yml"] diff --git a/Dockerfile.web b/Dockerfile.web new file mode 100644 index 0000000..87af5ac --- /dev/null +++ b/Dockerfile.web @@ -0,0 +1,31 @@ +# ── Stage 1: Next.js Build ──────────────────────────────────────────────────── +FROM node:22-bookworm-slim AS node-builder + +WORKDIR /build +COPY package.json package-lock.json ./ +RUN npm ci + +COPY . . +RUN npm run build + +# ── Stage 2: Runtime (Next.js standalone) ───────────────────────────────────── +FROM node:22-bookworm-slim + +WORKDIR /app + +RUN useradd --system --shell /bin/false --uid 998 --create-home nextjs + +COPY --from=node-builder /build/.next/standalone ./ +COPY --from=node-builder /build/.next/static ./.next/static +COPY --from=node-builder /build/public ./public + +RUN chown -R nextjs:nextjs /app + +USER nextjs +EXPOSE 3000 + +ENV PORT=3000 +ENV HOSTNAME=0.0.0.0 +# NEXT_PUBLIC_API_URL wird zur Laufzeit gesetzt (docker-compose environment) + +CMD ["node", "server.js"] diff --git a/config/config.docker.yml.example b/config/config.docker.yml.example new file mode 100644 index 0000000..25cc187 --- /dev/null +++ b/config/config.docker.yml.example @@ -0,0 +1,57 @@ +# archivmail Docker-Konfiguration +# Kopieren nach: /etc/archivmail/config.yml +# Wird als Read-Only-Volume in den Container gemountet. + +server: + api_port: 8080 + smtp_port: 2525 + +database: + host: postgres # Docker Compose Service-Name (nicht localhost!) + port: 5432 + name: archivmail + user: archivmail + password: CHANGE_ME_DB_PASSWORD + sslmode: disable + +storage: + store_path: /var/archivmail/store + astore_path: /var/archivmail/astore + xapian_path: /var/archivmail/xapian + keyfile: /etc/archivmail/keyfile + +index: + path: /var/archivmail/xapian + backend: xapian + batch_size: 100 + async_queue_size: 1000 + +api: + bind: "0.0.0.0:8080" + secret: CHANGE_ME_64_CHAR_SECRET + secure_cookies: true + trusted_proxies: + - "172.16.0.0/12" # Docker-interne Netzwerke + - "127.0.0.1" + +smtp: + enabled: true + bind: "0.0.0.0:2525" + domain: mail.example.com + max_size_mb: 25 + allowed_ips: + - "0.0.0.0/0" + - "::/0" + tenant_routing: default + default_tenant_id: 1 + +imap_server: + enabled: false + bind: "0.0.0.0:1143" + +audit: + log_path: /var/archivmail/audit.log + retention_days: 365 + +logging: + level: info diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a15fab9 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,54 @@ +services: + + # ── PostgreSQL ────────────────────────────────────────────────────────────── + postgres: + image: postgres:16-bookworm + restart: unless-stopped + environment: + POSTGRES_DB: archivmail + POSTGRES_USER: archivmail + POSTGRES_PASSWORD: ${DB_PASSWORD} + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U archivmail"] + interval: 10s + timeout: 5s + retries: 5 + + # ── Go Backend ────────────────────────────────────────────────────────────── + archivmail: + build: + context: . + dockerfile: Dockerfile + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + volumes: + - /etc/archivmail:/etc/archivmail:ro + - archivmail_data:/var/archivmail + ports: + - "127.0.0.1:8080:8080" # API (nginx proxied) + - "2525:2525" # SMTP BCC + - "1143:1143" # IMAP (nginx proxied für TLS) + environment: + - TZ=Europe/Berlin + + # ── Next.js Frontend ──────────────────────────────────────────────────────── + archivmail-web: + build: + context: . + dockerfile: Dockerfile.web + restart: unless-stopped + depends_on: + - archivmail + ports: + - "127.0.0.1:3000:3000" # Frontend (nginx proxied) + environment: + - NEXT_PUBLIC_API_URL=http://archivmail:8080 + - TZ=Europe/Berlin + +volumes: + postgres_data: + archivmail_data: diff --git a/features/INDEX.md b/features/INDEX.md index a4b3650..2b21c82 100644 --- a/features/INDEX.md +++ b/features/INDEX.md @@ -42,14 +42,14 @@ | PROJ-26 | IMAP-Server-Schnittstelle (Read-Only Archivzugriff) | In Progress | [PROJ-26](PROJ-26-imap-server-schnittstelle.md) | 2026-03-18 | | PROJ-27 | Container-Ready (Dockerfile + Env-Vars) | In Review | [PROJ-27](PROJ-27-container-ready.md) | 2026-03-28 | -| PROJ-28 | Self-Service Onboarding (Sign-up, E-Mail-Verifikation, Passwort-Reset) | Planned | [PROJ-28](PROJ-28-self-service-onboarding.md) | 2026-03-28 | +| PROJ-28 | Self-Service Onboarding (Sign-up, E-Mail-Verifikation, Passwort-Reset) | In Progress | [PROJ-28](PROJ-28-self-service-onboarding.md) | 2026-03-28 | | PROJ-29 | Tenant-Quotas & Usage-Limits | Planned | [PROJ-29](PROJ-29-tenant-quotas.md) | 2026-03-28 | | PROJ-30 | Volltext-Index: Xapian → Manticore Search Migration | Planned | [PROJ-30](PROJ-30-bleve-migration.md) | 2026-03-28 | | PROJ-31 | Billing & Subscriptions (Stripe) | Planned | [PROJ-31](PROJ-31-billing-subscriptions.md) | 2026-03-28 | -| PROJ-32 | Message-ID-basierte Duplikatserkennung | In Progress | [PROJ-32](PROJ-32-message-id-dedup.md) | 2026-03-31 | -| PROJ-33 | IMAP-Modus: Gemeinsames Archiv vs. Persönlicher Posteingang | Planned | [PROJ-33](PROJ-33-imap-modus-shared-personal.md) | 2026-03-31 | -| PROJ-34 | Retention-Policy + Löschsperre (GoBD-Compliance) | In Progress | [PROJ-34](PROJ-34-retention-policy.md) | 2026-03-31 | +| PROJ-32 | Message-ID-basierte Duplikatserkennung | Deployed | [PROJ-32](PROJ-32-message-id-dedup.md) | 2026-03-31 | +| PROJ-33 | IMAP-Modus: Gemeinsames Archiv vs. Persönlicher Posteingang | Deployed | [PROJ-33](PROJ-33-imap-modus-shared-personal.md) | 2026-03-31 | +| PROJ-34 | Retention-Policy + Löschsperre (GoBD-Compliance) | Deployed | [PROJ-34](PROJ-34-retention-policy.md) | 2026-03-31 | diff --git a/install.sh b/install.sh index 49cd32e..0c42277 100755 --- a/install.sh +++ b/install.sh @@ -1,8 +1,11 @@ #!/bin/bash -# archivmail Server-Installer +# archivmail Installer # Unterstützte Systeme: Debian 12 / 13 -# Aufruf: bash install.sh -# Mit eigenem DB-Passwort: DB_PASSWORD=geheim bash install.sh +# +# 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 @@ -15,8 +18,11 @@ die() { echo -e "${RED}[ERR]${NC} $*" >&2; exit 1; } [[ $EUID -eq 0 ]] || die "Bitte als root ausführen: sudo bash install.sh" +# ── 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" @@ -24,106 +30,444 @@ 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}" +REPO_URL="${REPO_URL:-https://github.com/yourorg/archivmail.git}" +# ── Banner ──────────────────────────────────────────────────────────────────── echo "" -echo " ╔══════════════════════════════════════╗" -echo " ║ archivmail Installer v1.1 ║" -echo " ╚══════════════════════════════════════╝" +echo " ╔══════════════════════════════════════════╗" +echo " ║ archivmail Installer v2.0 ║" +echo " ╚══════════════════════════════════════════╝" echo "" info "Hostname: $FQDN" echo "" -# ── 1. Pakete ───────────────────────────────────────────────────────────────── -info "Installiere Systempakete..." -apt-get update -qq -apt-get install -y -qq \ - golang-go nodejs npm postgresql nginx \ - libxapian-dev pkg-config build-essential \ - curl git rsync logrotate openssl -log "Pakete installiert" +# ── Modus-Auswahl ───────────────────────────────────────────────────────────── +INSTALL_MODE="${INSTALL_MODE:-}" -# go im PATH sicherstellen (Debian legt binary nicht immer in /usr/bin) -if ! command -v go >/dev/null 2>&1; then - GO_BIN=$(find /usr/lib/go-*/bin /usr/local/go/bin -name "go" 2>/dev/null | head -1) - [[ -n "$GO_BIN" ]] || die "go binary nicht gefunden nach Installation" - ln -sf "$GO_BIN" /usr/local/bin/go - log "go binary verlinkt: $GO_BIN → /usr/local/bin/go" -fi -log "go $(go version | awk '{print $3}')" - -# ── 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"; } - -# ── 3. Verzeichnisstruktur ──────────────────────────────────────────────────── -info "Erstelle Verzeichnisstruktur..." -mkdir -p "$STORE_DIR/store" "$STORE_DIR/astore" "$STORE_DIR/xapian" -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" "$STORE_DIR/xapian" -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" -else - log "Keyfile existiert bereits – wird nicht überschrieben" +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, Xapian, Node.js, PostgreSQL)" + 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 -# ── 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 +[[ "$INSTALL_MODE" == "native" || "$INSTALL_MODE" == "docker" ]] \ + || die "INSTALL_MODE muss 'native' oder 'docker' sein (aktuell: $INSTALL_MODE)" -# ── 6. PostgreSQL ───────────────────────────────────────────────────────────── -info "Richte PostgreSQL ein..." -systemctl enable postgresql --quiet -systemctl start postgresql +echo "" +info "Modus: $INSTALL_MODE" +echo "" -# 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 +# ══════════════════════════════════════════════════════════════════════════════ +# DOCKER-INSTALLATION +# ══════════════════════════════════════════════════════════════════════════════ +install_docker() { -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 + # GitHub-Repo abfragen wenn nicht gesetzt + if [[ "$REPO_URL" == *"yourorg"* ]]; then + read -rp " GitHub-Repository URL (z.B. https://github.com/org/archivmail.git): " REPO_URL + [[ -n "$REPO_URL" ]] || die "Kein Repository angegeben." + fi -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 + # ── 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 -log "PostgreSQL eingerichtet" -log "Standard-Benutzer werden beim ersten Daemon-Start angelegt" + # ── Hilfspakete ─────────────────────────────────────────────────────────── + info "Installiere Hilfspakete..." + apt-get install -y -qq git curl nginx openssl logrotate + log "Pakete installiert" -# ── 7. Konfiguration ────────────────────────────────────────────────────────── -info "Erstelle Konfigurationsdatei..." -if [ ! -f "$CONFIG_DIR/config.yml" ]; then + # ── 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" + 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 2>&1; then + GO_BIN=$(find /usr/lib/go-*/bin /usr/local/go/bin -name "go" 2>/dev/null | head -1) + [[ -n "$GO_BIN" ]] || die "go binary nicht gefunden nach Installation" + ln -sf "$GO_BIN" /usr/local/bin/go + log "go binary verlinkt: $GO_BIN → /usr/local/bin/go" + fi + log "go $(go version | awk '{print $3}')" + + # ── 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"; } + + # ── 3. Verzeichnisstruktur ──────────────────────────────────────────────── + info "Erstelle Verzeichnisstruktur..." + mkdir -p "$STORE_DIR/store" "$STORE_DIR/astore" "$STORE_DIR/xapian" + 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" "$STORE_DIR/xapian" + 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" + 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: @@ -175,16 +519,16 @@ imap_server: 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 + 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 (HTTP + HTTPS) ──────────────────────────────────────────────────── -info "Konfiguriere Nginx (HTTP → HTTPS Redirect + TLS)..." -cat > /etc/nginx/sites-available/archivmail << NGINX + # ── 8. Nginx ────────────────────────────────────────────────────────────── + info "Konfiguriere nginx (HTTP → HTTPS + TLS)..." + cat > /etc/nginx/sites-available/archivmail << NGINX # HTTP → HTTPS Redirect server { listen 80; @@ -236,15 +580,15 @@ server { 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 (HTTP→HTTPS, TLS)" + 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 + # ── 9. logrotate ────────────────────────────────────────────────────────── + cat > /etc/logrotate.d/archivmail << LOGROTATE ${LOG_DIR}/audit.log { daily rotate 365 @@ -255,11 +599,11 @@ ${LOG_DIR}/audit.log { create 640 ${AM_USER} ${AM_USER} } LOGROTATE -log "logrotate konfiguriert" + log "logrotate konfiguriert" -# ── 10. systemd Units ───────────────────────────────────────────────────────── -info "Erstelle systemd Units..." -cat > /etc/systemd/system/archivmail.service << UNIT + # ── 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 @@ -286,7 +630,7 @@ ReadOnlyPaths=${CONFIG_DIR} ${SSL_DIR} WantedBy=multi-user.target UNIT -cat > /etc/systemd/system/archivmail-web.service << UNIT + cat > /etc/systemd/system/archivmail-web.service << UNIT [Unit] Description=archivmail Web Frontend After=network.target archivmail.service @@ -310,134 +654,108 @@ SyslogIdentifier=archivmail-web WantedBy=multi-user.target UNIT -systemctl daemon-reload -systemctl enable archivmail archivmail-web --quiet -log "systemd Units erstellt und aktiviert" + systemctl daemon-reload + systemctl enable archivmail archivmail-web --quiet + log "systemd Units erstellt und aktiviert" -# ── 11. update.sh installieren und erstes Deployment ────────────────────────── -info "Installiere update.sh und führe erstes Deployment durch..." -curl -fsSL "${REPO_URL/\.git/}/raw/branch/main/update.sh" -o "$INSTALL_DIR/update.sh" 2>/dev/null \ - || { warn "update.sh konnte nicht von Gitea geladen werden – überspringe Deployment"; } + # ── 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" + # GitHub-URLs anpassen (raw.githubusercontent.com statt github.com/raw/...) + if [[ "$REPO_URL" == *"github.com"* ]]; then + _repo_path="${REPO_URL#*github.com/}" + _repo_path="${_repo_path%.git}" + _update_url="https://raw.githubusercontent.com/${_repo_path}/main/update.sh" + fi -if [ -f "$INSTALL_DIR/update.sh" ]; then - chmod +x "$INSTALL_DIR/update.sh" - log "update.sh installiert: $INSTALL_DIR/update.sh" - info "Führe erstes Deployment durch (Build + Start)..." - bash "$INSTALL_DIR/update.sh" - # Echte Passwörter aus Journal lesen (Backend gibt sie beim ersten Start aus) - # Auf Backend warten (bis zu 15 Sekunden) - 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 — siehe: journalctl -u archivmail | grep admin)" - [[ -n "$PW_AUDITOR" ]] || PW_AUDITOR="(nicht gefunden — siehe: journalctl -u archivmail | grep auditor)" - [[ -n "$PW_SUPERADMIN" ]] || PW_SUPERADMIN="(nicht gefunden — siehe: journalctl -u archivmail | grep superadmin)" -else - warn "update.sh fehlt — manuell ausführen: bash $INSTALL_DIR/update.sh" - PW_SUPERADMIN="(noch nicht generiert)" - PW_ADMIN="(noch nicht generiert)" - PW_AUDITOR="(noch nicht generiert)" -fi + curl -fsSL "$_update_url" -o "$INSTALL_DIR/update.sh" 2>/dev/null \ + || { warn "update.sh konnte nicht geladen werden – überspringe Deployment"; } -# ── Zusammenfassung in Datei speichern ──────────────────────────────────────── -SUMMARY_FILE="$CONFIG_DIR/install-summary.txt" -cat > "$SUMMARY_FILE" << SUMMARY -archivmail Installation — $(date '+%d.%m.%Y %H:%M:%S') + 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 -=== ANGELEGTE BENUTZER & PASSWÖRTER === +=== ZUGANGSDATEN === - Systembenutzer (Linux): - Benutzer: $AM_USER - Shell: /bin/false (kein Login) - Home: $STORE_DIR + Datenbank: archivmail / $DB_PASSWORD + API-Secret: $API_SECRET - Datenbank (PostgreSQL): - Datenbank: archivmail - Benutzer: archivmail - Passwort: $DB_PASSWORD - Verbindung: 127.0.0.1:5432 - - API-Secret (JWT + AES-Schlüsselableitung): - Secret: $API_SECRET - - Web-Anwendung (UNBEDINGT ÄNDERN!): - Superadmin: superadmin@archivmail / $PW_SUPERADMIN - Admin: admin@archivmail / $PW_ADMIN - Auditor: auditor@archivmail / $PW_AUDITOR + 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 + 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 - TLS-Schlüssel: $SSL_DIR/archivmail.key Mail-Speicher: $STORE_DIR/ Logs: $LOG_DIR/ Updater: $INSTALL_DIR/update.sh - -=== HINWEISE === - - - Das TLS-Zertifikat ist selbstsigniert (10 Jahre gültig). - Im Browser: Ausnahme hinzufügen oder Zertifikat importieren. - Für vertrauenswürdiges Zertifikat: Admin → Zertifikat → Let's Encrypt - - DB-Passwort und API-Secret stehen in $CONFIG_DIR/config.yml (chmod 640). - - Keyfile NIEMALS löschen oder überschreiben — alle Mails werden damit entschlüsselt. - - Diese Datei nach dem ersten Login löschen oder sicher aufbewahren! SUMMARY -chmod 600 "$SUMMARY_FILE" -log "Zusammenfassung gespeichert: $SUMMARY_FILE" + chmod 600 "$SUMMARY_FILE" + log "Zusammenfassung: $SUMMARY_FILE" -# ── Abschlussbericht ────────────────────────────────────────────────────────── -echo "" -echo " ╔══════════════════════════════════════════════════════════╗" -echo " ║ Installation abgeschlossen! ║" -echo " ╚══════════════════════════════════════════════════════════╝" -echo "" -echo " Hostname (FQDN): $FQDN" -echo "" -echo " Pfade:" -echo " Binaries: $INSTALL_DIR/" -echo " Mail-Speicher: $STORE_DIR/" -echo " Konfiguration: $CONFIG_DIR/config.yml" -echo " Keyfile: $CONFIG_DIR/keyfile (chmod 400 – sicher aufbewahren!)" -echo " TLS-Zertifikat: $SSL_DIR/archivmail.crt" -echo " Logs: $LOG_DIR/" -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 " ├─────────────────────────────────────────────────────────┤" -printf " │ API-Secret: %-38s │\n" "${API_SECRET:0:38}" -echo " └─────────────────────────────────────────────────────────┘" -echo "" -echo " Vollständige Zusammenfassung: $SUMMARY_FILE" -echo "" -echo " Dienste:" -echo " Web (HTTPS): https://$FQDN" -echo " IMAP (TLS): $FQDN:993" -echo " SMTP: $FQDN:2525" -echo "" -echo " Hinweis: Das TLS-Zertifikat ist selbstsigniert." -echo " Für ein vertrauenswürdiges Zertifikat: Admin → Zertifikat → Let's Encrypt" -echo "" -warn "Zusammenfassung mit Passwörtern: $SUMMARY_FILE (chmod 600)" -warn "Standardpasswörter unbedingt nach dem ersten Login ändern!" -echo "" + # ── 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 diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..3618f39 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# deploy.sh — Wird vom Webhook-Dienst bei GitHub-Push auf main aufgerufen. +# Läuft auf dem HOST (nicht im Container). + +set -euo pipefail + +INSTALL_DIR="${INSTALL_DIR:-/opt/archivmail}" +LOG_FILE="/var/log/archivmail/deploy.log" + +log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"; } + +log "=== Deploy gestartet (von GitHub Webhook) ===" + +cd "$INSTALL_DIR" + +# 1. Aktuellen Stand holen +log "git pull origin main..." +git pull origin main + +# 2. Neue Images bauen +log "docker compose build..." +docker compose build --no-cache + +# 3. Stack neustarten (zero-downtime: postgres bleibt laufen) +log "docker compose up -d..." +docker compose up -d --remove-orphans + +# 4. Alte ungenutzte Images aufräumen +docker image prune -f + +log "=== Deploy abgeschlossen ===" diff --git a/scripts/webhook-hooks.json b/scripts/webhook-hooks.json new file mode 100644 index 0000000..fe4fe35 --- /dev/null +++ b/scripts/webhook-hooks.json @@ -0,0 +1,32 @@ +[ + { + "id": "deploy", + "execute-command": "/opt/archivmail/scripts/deploy.sh", + "command-working-directory": "/opt/archivmail", + "response-message": "Deploy gestartet.", + "trigger-rule": { + "and": [ + { + "match": { + "type": "payload-hmac-sha256", + "secret": "{{ getenv \"WEBHOOK_SECRET\" }}", + "parameter": { + "source": "header", + "name": "X-Hub-Signature-256" + } + } + }, + { + "match": { + "type": "value", + "value": "refs/heads/main", + "parameter": { + "source": "payload", + "name": "ref" + } + } + } + ] + } + } +]