diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ac27556 --- /dev/null +++ b/.env.example @@ -0,0 +1,55 @@ +# Kopie als deploy/.env ablegen und Werte setzen. .env NIE committen. + +# --- Postgres --- +# Nur URL-sichere Zeichen (A-Za-z0-9_-): das Passwort landet auch in Hydras DSN-URL. +PG_PASSWORD=CHANGE_ME + +# --- Hydra --- +# openssl rand -hex 32 — nach Erstinbetriebnahme NICHT mehr ändern. +SECRETS_SYSTEM=CHANGE_ME +# Öffentlicher OAuth-Issuer (Reverse-Proxy-vHost auf hydra:4444). MUSS im Betrieb +# per HTTPS erreichbar sein — der MCP-Server validiert JWTs dagegen mit +# RequireHttpsMetadata=true. Für reinen lokalen Smoke ohne Token-Flow: +# http://127.0.0.1:4444 (der MCP-200-Tool-Call gelingt damit NICHT, siehe README). +PUBLIC_ISSUER=https://mcp-oauth.example.com +# Basis-URL des Login-Services (Reverse-Proxy-vHost auf login:8081). +LOGIN_URL=https://mcp-login.example.com + +# --- MCP --- +# Öffentliche Basis-URL des MCP-Resource-Servers (Reverse-Proxy-vHost auf mcp:8080). +MCP_RESOURCE_URL=https://mcp.example.com + +# --- SAP Service Layer --- +# Trailing /b1s/ ist Pflicht (relative Pfade wie v2/Login). +SL_BASE_URL=https://sap-server.example.com:50000/b1s/ +SL_COMPANYDB=CHANGE_ME +# true nur bei self-signed SAP-Zertifikat. +SL_INSECURE=false + +# --- Admin-UI --- +# Nur dieser SAP-Benutzer darf das Admin-UI öffnen (gegen den Service Layer geprüft). +# PFLICHT — ohne diesen Wert lässt der Admin-Dienst niemanden hinein (fail closed). +ADMIN_SAP_USER=manager + +# --- MCP-Zugriff (Allowlist) --- +# Kommagetrennte SAP-UserCodes, die den MCP IMMER nutzen dürfen. Alle anderen sind +# per Default AUS und werden im Admin-UI unter „Zugriff" freigeschaltet. +MCP_ALLOWED_USERS=manager + +# --- Host-Ports / Bind-IPs (für den Reverse Proxy) --- +MCP_HOST_PORT=8080 +LOGIN_HOST_PORT=8081 +ADMIN_HOST_PORT=8082 +HYDRA_PUBLIC_HOST_PORT=4444 +# NUR an die interne Host-IP binden, die der Reverse Proxy erreicht — NIE 0.0.0.0. +# Default 127.0.0.1 ist sicher (Proxy auf demselben Host). +#BIND_ADDR=127.0.0.1 +# Bind-IP der Admin-UI (inkl. /audit hinter dem SAP-Login-Gate): Default 127.0.0.1. +#ADMIN_BIND=127.0.0.1 + +# --- Deployment (setzt setup.sh automatisch) --- +# Host-Pfad des Deployments — braucht die Update-Funktion des Admin-UI. +PROJECT_DIR= + +# Hinweis: CREDS_KEY kommt NICHT aus .env, sondern als Secret-Datei +# deploy/secrets/creds_key (32 Byte base64: openssl rand -base64 32). diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..0dbb11c --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,105 @@ +# EasyMCP / SapB1Mcp — Installation (Kunden-Deployment) + +Der Stack wird direkt aus der öffentlichen Registry von `git.kraemerweb.de` +installiert und aktualisiert — es wird kein Installationspaket kopiert. + +## Voraussetzungen + +- Linux-Host, x86_64 (getestet: Debian 12/13) +- Docker Engine ≥ 24 mit Compose-Plugin v2 (`docker compose version`) + — alternativ ein vorhandenes `docker-compose`-Binary +- `curl` +- **HTTPS-Zugriff auf `git.kraemerweb.de`** (Release-Dateien + Container-Registry) +- Ein Reverse Proxy mit **TLS** (z. B. Caddy, nginx, Traefik) vor dem Stack. + HTTPS ist Pflicht: der MCP-Server validiert OAuth-Tokens nur gegen einen + HTTPS-Issuer. +- Erreichbarer SAP Business One Service Layer (`https://:50000/b1s/`) + +## Installation + +1. **Setup-Skript holen** (z. B. nach `/opt/sapb1mcp`): + + ``` + mkdir -p /opt/sapb1mcp && cd /opt/sapb1mcp + curl -fsSLO https://git.kraemerweb.de/kraemerweb/easymcp-release/raw/branch/main/setup.sh + bash setup.sh + ``` + + `bash setup.sh ` installiert eine bestimmte Version (Tags siehe + Release-Repo); ohne Angabe wird `latest` verwendet. Das Skript lädt + `docker-compose.yml`, `hydra.yml` und `.env.example`, erzeugt + `secrets/creds_key` und eine `.env` mit fertigen Zufalls-Secrets. + +2. **`.env` anpassen** — Pflichtwerte: + + | Variable | Bedeutung | + |---|---| + | `PUBLIC_ISSUER` | öffentliche HTTPS-URL des OAuth-Issuers (vHost → Port 4444) | + | `LOGIN_URL` | öffentliche HTTPS-URL des Login-Dienstes (vHost → Port 8081) | + | `MCP_RESOURCE_URL` | öffentliche HTTPS-URL des MCP-Servers (vHost → Port 8080) | + | `SL_BASE_URL` | SAP Service Layer, trailing `/b1s/` ist Pflicht | + | `SL_COMPANYDB` | SAP-Firmendatenbank | + | `ADMIN_SAP_USER` | SAP-Benutzer mit Zugang zum Admin-UI (Port 8082) | + | `MCP_ALLOWED_USERS` | SAP-Benutzer mit MCP-Dauerzugriff (Allowlist, default-deny) | + + `PG_PASSWORD`, `SECRETS_SYSTEM`, `APP_VERSION` und `PROJECT_DIR` hat + `setup.sh` bereits gesetzt. `SL_INSECURE=true` nur bei self-signed + SAP-Zertifikat. + +3. **Stack starten:** + + ``` + docker compose pull + docker compose up -d + ``` + +4. **Verifizieren:** + + ``` + docker compose ps # alle Dauerläufer "healthy" + curl -s http://127.0.0.1:8080/.well-known/oauth-protected-resource + ``` + +## Reverse Proxy + +Alle Dienste binden per Default **nur an 127.0.0.1**. Läuft der Reverse Proxy +auf einem anderen Host, in `.env` `BIND_ADDR`/`ADMIN_BIND` auf die interne +Host-IP setzen — **nie 0.0.0.0**. + +| vHost (Beispiel) | Backend-Port | Dienst | +|---|---|---| +| `mcp.` | 8080 | MCP-Resource-Server | +| `mcp-oauth.` | 4444 | Hydra (OAuth-Issuer) | +| `mcp-login.` | 8081 | Login/Consent | +| `mcp-admin.` (intern!) | 8082 | Admin-UI | + +Der Proxy muss `X-Forwarded-Proto: https` setzen (TLS-Terminierung). +Das Admin-UI gehört NICHT ins öffentliche Internet. + +## Update auf eine neue Version + +**Weg 1 — über das Admin-UI (empfohlen):** Seite **Update** öffnen, optional +eine Zielversion eintragen (leer = `latest`), „Update starten" klicken. Der +Stack holt die neuen Images aus der Registry und erneuert alle Dienste selbst +(`.env`, `secrets/` und die Datenbank bleiben erhalten). Dabei startet auch +das Admin-UI neu — danach neu anmelden. + +**Weg 2 — manuell per SSH:** + +``` +bash setup.sh [version] # aktualisiert Deployment-Dateien + APP_VERSION +docker compose pull +docker compose up -d +``` + +## Rollback + +Wie ein Update, nur mit einer älteren Versionsnummer als Zielversion +(Admin-UI-Feld „Zielversion" bzw. `bash setup.sh `). + +## Backup + +- Docker-Volume `sapb1mcp_pgdata` (Postgres: Benutzer, Audit, Hydra-Clients) +- `secrets/creds_key` — ohne diese Datei sind gespeicherte SAP-Zugangsdaten + unwiederbringlich verloren +- `.env` (enthält `SECRETS_SYSTEM` — nach Inbetriebnahme nie ändern) diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..ada9862 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +2026.07.07-24520d4 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..53b0996 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,291 @@ +# SAP-MCP-Stack (C#) — RELEASE-Compose für Kunden-Deployments (liegt via CI als +# docker-compose.yml im öffentlichen Repo kraemerweb/easymcp-release). Referenziert +# NUR vorgebaute Images aus der öffentlichen Gitea-Registry (kein build:) — +# der Kunde zieht Updates per 'docker compose pull && docker compose up -d'. +# APP_VERSION setzt setup.sh in der .env (Default: latest, pinbar auf Versionstags). +# +# Härtung wie im Entwicklungs-Compose: alle Dienste no-new-privileges; die +# .NET-Dienste + Hydra zusätzlich cap_drop ALL; login/mcp/admin read_only +# (nur /tmp als tmpfs). Postgres behält Default-Caps und bleibt schreibbar. +name: sapb1mcp + +# Log-Rotation für die Dauerläufer (Docker-Default rotiert nicht). Max ~30 MB/Dienst. +x-logging: &default-logging + driver: json-file + options: + max-size: "10m" + max-file: "3" + +services: + postgres: + image: postgres:17-alpine + container_name: sapb1mcp-postgres + restart: unless-stopped + logging: *default-logging + security_opt: + - no-new-privileges:true + environment: + POSTGRES_USER: mcp + POSTGRES_PASSWORD: ${PG_PASSWORD:?PG_PASSWORD fehlt in .env — setup.sh ausführen} + POSTGRES_DB: mcp + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U mcp -d mcp"] + interval: 5s + timeout: 3s + retries: 12 + + # App-Schema (sap_credentials, mcp_user_access, audit_entries) via EF Core. + # Nutzt das mcp-Image, ruft es aber im --migrate-Modus auf und beendet sich. + db-migrate: + image: git.kraemerweb.de/kraemerweb/sapb1mcp-mcp:${APP_VERSION:-latest} + container_name: sapb1mcp-db-migrate + restart: "no" + security_opt: + - no-new-privileges:true + depends_on: + postgres: + condition: service_healthy + environment: + POSTGRES_CONNECTION: "Host=postgres;Port=5432;Database=mcp;Username=mcp;Password=${PG_PASSWORD}" + entrypoint: ["dotnet", "SapB1McpServer.dll", "--migrate"] + + # Hydra-Schema (eigene Tabellen in derselben DB). + hydra-migrate: + image: oryd/hydra:v2.3.0 + container_name: sapb1mcp-hydra-migrate + restart: "no" + security_opt: + - no-new-privileges:true + depends_on: + postgres: + condition: service_healthy + environment: + DSN: postgres://mcp:${PG_PASSWORD}@postgres:5432/mcp?sslmode=disable + volumes: + - ./hydra.yml:/etc/hydra/hydra.yml:ro + command: migrate sql -e --yes --config /etc/hydra/hydra.yml + + hydra: + image: oryd/hydra:v2.3.0 + container_name: sapb1mcp-hydra + restart: unless-stopped + logging: *default-logging + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + depends_on: + hydra-migrate: + condition: service_completed_successfully + environment: + DSN: postgres://mcp:${PG_PASSWORD}@postgres:5432/mcp?sslmode=disable + SECRETS_SYSTEM: ${SECRETS_SYSTEM:?SECRETS_SYSTEM fehlt in .env — setup.sh ausführen} + URLS_SELF_ISSUER: ${PUBLIC_ISSUER:?PUBLIC_ISSUER fehlt in .env} + URLS_LOGIN: ${LOGIN_URL}/login + URLS_CONSENT: ${LOGIN_URL}/consent + # Hydra leitet registration_endpoint NICHT automatisch ab — ohne diesen + # Eintrag fehlt es im Discovery-Dokument und Clients halten DCR für + # nicht unterstützt. + WEBFINGER_OIDC_DISCOVERY_CLIENT_REGISTRATION_URL: ${PUBLIC_ISSUER}/oauth2/register + volumes: + - ./hydra.yml:/etc/hydra/hydra.yml:ro + command: serve all --config /etc/hydra/hydra.yml + ports: + # Nur an die interne Host-IP (BIND_ADDR) binden, die der Reverse Proxy + # erreicht — nie 0.0.0.0. Default 127.0.0.1 = sicher (Proxy auf demselben Host). + - "${BIND_ADDR:-127.0.0.1}:${HYDRA_PUBLIC_HOST_PORT:-4444}:4444" # öffentlich (OAuth-Issuer) + # Admin-API 4445 bleibt intern im Compose-Netz (nur login/admin-Container). + + login: + image: git.kraemerweb.de/kraemerweb/sapb1mcp-login:${APP_VERSION:-latest} + container_name: sapb1mcp-login + restart: unless-stopped + logging: *default-logging + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + read_only: true + tmpfs: + - /tmp + depends_on: + db-migrate: + condition: service_completed_successfully + hydra: + condition: service_started + environment: + ASPNETCORE_URLS: http://0.0.0.0:8081 + POSTGRES_CONNECTION: "Host=postgres;Port=5432;Database=mcp;Username=mcp;Password=${PG_PASSWORD}" + Hydra__AdminUrl: http://hydra:4445 + SAPServiceLayer__BaseUrl: ${SL_BASE_URL:?SL_BASE_URL fehlt in .env} + SAPServiceLayer__CompanyDB: ${SL_COMPANYDB:?SL_COMPANYDB fehlt in .env} + SAPServiceLayer__IgnoreSslErrors: ${SL_INSECURE:-false} + CREDS_KEY_FILE: /run/secrets/creds_key + # MCP-Zugriff: immer erlaubte SAP-Benutzer (Allowlist, default-deny für den Rest). + MCP_ALLOWED_USERS: ${MCP_ALLOWED_USERS:-manager} + secrets: + - creds_key + ports: + - "${BIND_ADDR:-127.0.0.1}:${LOGIN_HOST_PORT:-8081}:8081" + healthcheck: + test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8081/healthz"] + interval: 10s + timeout: 3s + retries: 6 + + mcp: + image: git.kraemerweb.de/kraemerweb/sapb1mcp-mcp:${APP_VERSION:-latest} + container_name: sapb1mcp-mcp + restart: unless-stopped + logging: *default-logging + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + read_only: true + tmpfs: + - /tmp + depends_on: + db-migrate: + condition: service_completed_successfully + hydra: + condition: service_started + environment: + McpHttp__Port: "8080" + # Öffentliche Resource-URL dieses MCP-Servers (RFC-9728-Metadaten + WWW-Authenticate). + MCP_RESOURCE_URL: ${MCP_RESOURCE_URL:?MCP_RESOURCE_URL fehlt in .env} + # Hydra-Issuer für JWT-Validierung (Authority). MUSS per HTTPS erreichbar sein + # (RequireHttpsMetadata=true) — im Betrieb der Reverse-Proxy-vHost. + Hydra__PublicIssuer: ${PUBLIC_ISSUER} + SAPServiceLayer__BaseUrl: ${SL_BASE_URL} + SAPServiceLayer__CompanyDB: ${SL_COMPANYDB} + SAPServiceLayer__IgnoreSslErrors: ${SL_INSECURE:-false} + POSTGRES_CONNECTION: "Host=postgres;Port=5432;Database=mcp;Username=mcp;Password=${PG_PASSWORD}" + CREDS_KEY_FILE: /run/secrets/creds_key + MCP_ALLOWED_USERS: ${MCP_ALLOWED_USERS:-manager} + secrets: + - creds_key + ports: + - "${BIND_ADDR:-127.0.0.1}:${MCP_HOST_PORT:-8080}:8080" + healthcheck: + # Der MCP-Server hat kein /healthz; die anonyme RFC-9728-Metadaten-Route + # antwortet ohne Token mit 200 und dient als Health-Signal. + test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/.well-known/oauth-protected-resource"] + interval: 10s + timeout: 3s + retries: 6 + + # Docker-Socket-Proxy: EINZIGER Container, der den Docker-Socket sieht. Filtert die + # Engine-API auf das Minimum, das das Admin-UI braucht (Container listen/Logs/Restart). + docker-socket-proxy: + image: tecnativa/docker-socket-proxy:0.3.0 + container_name: sapb1mcp-docker-proxy + restart: unless-stopped + logging: *default-logging + security_opt: + - no-new-privileges:true + # Kein read_only: der tecnativa-Entrypoint generiert seine haproxy.cfg zur Laufzeit + # aus der im Image liegenden Template — read_only dort crasht den Proxy. + environment: + # Nur Container-Bereich + POST (für /restart) freigeben, alles andere bleibt 0. + CONTAINERS: 1 + POST: 1 + # Explizit geschlossen halten (Defaults, zur Klarheit dokumentiert): + EXEC: 0 + IMAGES: 0 + VOLUMES: 0 + NETWORKS: 0 + INFO: 0 + SERVICES: 0 + TASKS: 0 + SWARM: 0 + SYSTEM: 0 + AUTH: 0 + SECRETS: 0 + CONFIGS: 0 + NODES: 0 + PLUGINS: 0 + DISTRIBUTION: 0 + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + networks: + - dockerproxy + + admin: + image: git.kraemerweb.de/kraemerweb/sapb1mcp-admin:${APP_VERSION:-latest} + container_name: sapb1mcp-admin + restart: unless-stopped + logging: *default-logging + security_opt: + - no-new-privileges:true + # Kein Docker-Socket-Mount: der Admin erreicht die Engine-API ausschließlich + # über den gefilterten Proxy (siehe DOCKER_HOST). + cap_drop: + - ALL + read_only: true + tmpfs: + - /tmp + depends_on: + db-migrate: + condition: service_completed_successfully + docker-socket-proxy: + condition: service_started + environment: + ASPNETCORE_URLS: http://0.0.0.0:8082 + POSTGRES_CONNECTION: "Host=postgres;Port=5432;Database=mcp;Username=mcp;Password=${PG_PASSWORD}" + Hydra__AdminUrl: http://hydra:4445 + # Login-Gate: nur dieser SAP-User darf das Admin-UI öffnen (SL-verifiziert). + # Pflicht (fail-closed) — der Dienst lässt sonst niemanden hinein. + ADMIN_SAP_USER: ${ADMIN_SAP_USER:?ADMIN_SAP_USER fehlt in .env} + SAPServiceLayer__BaseUrl: ${SL_BASE_URL} + SAPServiceLayer__CompanyDB: ${SL_COMPANYDB} + SAPServiceLayer__IgnoreSslErrors: ${SL_INSECURE:-false} + MCP_ALLOWED_USERS: ${MCP_ALLOWED_USERS:-manager} + # Container-Steuerung/Logs NUR über den gefilterten Proxy (nicht über den rohen Socket). + COMPOSE_PROJECT: sapb1mcp + DOCKER_HOST: tcp://docker-socket-proxy:2375 + # Update-Seite („Pull & Update"): startet den Einweg-Updater-Container. + # PROJECT_DIR = Host-Pfad des Deployments (setzt setup.sh); der Updater mountet + # ihn pfad-identisch, damit die relativen Binds der Compose aufgehen. + APP_VERSION: ${APP_VERSION:-latest} + PROJECT_DIR: ${PROJECT_DIR:?PROJECT_DIR fehlt in .env — setup.sh ausführen} + UPDATES_VOLUME: sapb1mcp_updates + # Updater bewusst auf :latest — er ist kein Compose-Dienst; setup.sh pullt ihn + # initial, updater-run.sh hält ihn bei jedem Update selbst aktuell. + UPDATER_IMAGE: git.kraemerweb.de/kraemerweb/sapb1mcp-updater:latest + volumes: + # Geteilte Ablage für update.log/update.status — so kann auch der beim + # Update NEU erstellte Admin das Ergebnis noch anzeigen. + - updates:/updates + networks: + - default + - dockerproxy + ports: + # Admin-UI inkl. /audit liegt hinter dem SAP-Login-Gate (nur /login + /healthz frei). + # Trotzdem nur intern binden: Default loopback; ADMIN_BIND in .env auf die interne + # Host-IP setzen, die der Reverse Proxy erreicht — nie 0.0.0.0. + - "${ADMIN_BIND:-127.0.0.1}:${ADMIN_HOST_PORT:-8082}:8082" + healthcheck: + test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8082/healthz"] + interval: 10s + timeout: 3s + retries: 6 + +volumes: + pgdata: + # Staging-Volume der Update-Funktion (Name auf dem Host: sapb1mcp_updates — + # muss zur UPDATES_VOLUME-Env des Admin passen, der Updater mountet es per Name). + updates: + +networks: + # Standard-Netz für alle Dienste (Postgres, Hydra, login, mcp, admin). + default: + # Isoliertes, nach außen abgeschottetes Netz nur für admin ↔ docker-socket-proxy. + dockerproxy: + internal: true + +secrets: + creds_key: + file: ./secrets/creds_key diff --git a/hydra.yml b/hydra.yml new file mode 100644 index 0000000..2285c91 --- /dev/null +++ b/hydra.yml @@ -0,0 +1,63 @@ +# Ory Hydra v2.3 — OAuth2-AS für den SAP-MCP-Stack (C#-Portierung). +# URLs/DSN/Secrets kommen aus dem Environment (compose): +# DSN, SECRETS_SYSTEM, URLS_SELF_ISSUER, URLS_LOGIN, URLS_CONSENT +serve: + cookies: + same_site_mode: Lax + tls: + # TLS terminiert der zentrale Reverse Proxy (kas-proxy/Caddy); interne Netze + # dürfen X-Forwarded-Proto setzen. + allow_termination_from: + - 10.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + public: + cors: + # claude.ai macht Discovery/DCR/Token-Calls teils aus dem Browser — + # ohne CORS meldet der Connector "Client-Registrierung nicht unterstützt". + enabled: true + allowed_origins: + - https://claude.ai + - https://claude.com + - https://chatgpt.com + - https://chat.openai.com + allowed_methods: + - GET + - POST + - OPTIONS + allowed_headers: + - Authorization + - Content-Type + exposed_headers: + - Content-Type + +strategies: + access_token: jwt + +oidc: + dynamic_client_registration: + # Self-Service-Onboarding: MCP-Clients registrieren sich anonym + # über POST /oauth2/register (RFC 7591). + enabled: true + # Clients, die bei der Registrierung KEINE Scopes angeben (z. B. ChatGPT), + # bekommen diese Defaults — sonst scheitert ihr Authorize-Request mit + # invalid_scope. ChatGPT fordert zusätzlich OIDC-Scopes (openid/profile/ + # email) an; die geben wir frei, auch wenn wir keine Profil-Claims führen. + default_scope: + - mcp + - offline_access + - openid + - profile + - email + +oauth2: + pkce: + enforced_for_public_clients: true + +ttl: + access_token: 1h + refresh_token: 720h + +log: + level: info + leak_sensitive_values: false diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..3a63371 --- /dev/null +++ b/setup.sh @@ -0,0 +1,159 @@ +#!/usr/bin/env bash +# EasyMCP / SapB1Mcp — Setup auf dem Docker-Host (Pull-Deployment). +# +# Die einzige Datei, die ein Kunde braucht. Bezug: +# curl -fsSLO https://git.kraemerweb.de/kraemerweb/easymcp-release/raw/branch/main/setup.sh +# +# Aufruf: +# bash setup.sh # neueste Version (latest) +# bash setup.sh # bestimmte Version (Tag im Release-Repo) +# +# Was das Skript tut: +# 1. Holt die Deployment-Dateien (docker-compose.yml, hydra.yml, .env.example) +# anonym aus dem öffentlichen Release-Repo — passend zur gewählten Version. +# 2. Erzeugt secrets/creds_key (32 Byte base64, Verschlüsselung der SAP-Credentials). +# 3. Legt .env aus .env.example an und generiert PG_PASSWORD + SECRETS_SYSTEM; +# setzt APP_VERSION (= gewählte Version bzw. latest) und PROJECT_DIR. +# 4. Pullt das Updater-Image (für die Update-Funktion des Admin-UI). +# +# Idempotent: eine vorhandene .env und ein vorhandener creds_key werden NIE +# überschrieben — nur APP_VERSION und PROJECT_DIR werden nachgezogen. Die +# Deployment-Dateien werden immer auf den Stand der gewählten Version gebracht. +set -euo pipefail +cd "$(dirname "$0")" + +RELEASE_BASE="${RELEASE_BASE:-https://git.kraemerweb.de/kraemerweb/easymcp-release}" +REGISTRY_IMAGE_BASE="${REGISTRY_IMAGE_BASE:-git.kraemerweb.de/kraemerweb}" + +VERSION="${1:-}" +if [ -n "$VERSION" ]; then + REF="tag/${VERSION}" + APP_VERSION="$VERSION" +else + REF="branch/main" + APP_VERSION="latest" +fi + +info() { printf '\033[1;34m==>\033[0m %s\n' "$*"; } +ok() { printf '\033[1;32m ✓\033[0m %s\n' "$*"; } +warn() { printf '\033[1;33m !\033[0m %s\n' "$*"; } +fail() { printf '\033[1;31mFEHLER:\033[0m %s\n' "$*" >&2; exit 1; } + +# --- Zufallswerte (openssl bevorzugt, /dev/urandom als Fallback) ------------- +rand_hex() { # $1 = Byte-Anzahl; Ausgabe hex (URL-sicher, auch für DSN-URLs) + if command -v openssl >/dev/null 2>&1; then + openssl rand -hex "$1" | tr -d ' \r\n' + else + od -An -N"$1" -tx1 /dev/urandom | tr -d ' \r\n' + fi +} +rand_b64() { # $1 = Byte-Anzahl; Ausgabe base64 (eine Zeile) + if command -v openssl >/dev/null 2>&1; then + openssl rand -base64 "$1" | tr -d '\r\n' + else + head -c "$1" /dev/urandom | base64 | tr -d '\r\n' + fi +} +set_env() { # $1 = Name, $2 = Wert — setzt bzw. ergänzt NAME=WERT in .env + if grep -q "^$1=" .env; then + sed -i "s|^$1=.*|$1=$2|" .env + else + printf '%s=%s\n' "$1" "$2" >> .env + fi +} +fetch() { # $1 = Datei im Release-Repo, $2 = Zielname — atomar ersetzen + curl -fsSL "$RELEASE_BASE/raw/$REF/$1" -o "$2.new" \ + || fail "Download fehlgeschlagen: $RELEASE_BASE/raw/$REF/$1 (Version/Netzwerk prüfen)" + mv "$2.new" "$2" +} + +# --- Vorbedingungen ----------------------------------------------------------- +command -v curl >/dev/null 2>&1 || fail "curl nicht gefunden — bitte installieren." +command -v docker >/dev/null 2>&1 || fail "docker nicht gefunden — bitte Docker Engine installieren." +docker info >/dev/null 2>&1 || fail "Docker-Daemon nicht erreichbar (läuft der Dienst? Rechte? Ggf. sudo verwenden)." + +if docker compose version >/dev/null 2>&1; then + COMPOSE="docker compose" +elif command -v docker-compose >/dev/null 2>&1; then + COMPOSE="docker-compose" +else + fail "Weder 'docker compose' (Plugin v2) noch 'docker-compose' gefunden." +fi + +# --- 1. Deployment-Dateien holen ----------------------------------------------- +info "Hole Deployment-Dateien (${VERSION:-latest}) aus $RELEASE_BASE ..." +fetch docker-compose.yml docker-compose.yml +fetch hydra.yml hydra.yml +fetch .env.example .env.example +ok "docker-compose.yml, hydra.yml, .env.example aktualisiert." + +# --- 2. creds_key -------------------------------------------------------------- +info "Prüfe secrets/creds_key ..." +mkdir -p secrets +if [ -f secrets/creds_key ]; then + ok "secrets/creds_key existiert bereits — bleibt unverändert." +else + ( umask 077; rand_b64 32 > secrets/creds_key ) + ok "secrets/creds_key erzeugt (chmod 600)." + echo " WICHTIG: Diese Datei SICHERN. Ohne sie sind die gespeicherten SAP-Zugangsdaten unlesbar." +fi + +# --- 3. .env ------------------------------------------------------------------- +info "Prüfe .env ..." +if [ -f .env ]; then + ok ".env existiert bereits — Secrets/Werte bleiben unverändert." + current="$(sed -n 's/^APP_VERSION=//p' .env | head -n1)" + set_env APP_VERSION "$APP_VERSION" + if [ -n "$current" ] && [ "$current" != "$APP_VERSION" ]; then + ok "Update: APP_VERSION ${current} -> ${APP_VERSION} (wirkt nach '${COMPOSE} pull' + '${COMPOSE} up -d')." + fi +else + cp .env.example .env + chmod 600 .env + PG_PASSWORD="$(rand_hex 24)" # nur [0-9a-f]: sicher für Hydras DSN-URL + SECRETS_SYSTEM="$(rand_hex 32)" # Hydra-Systemschlüssel — nach Inbetriebnahme NIE mehr ändern + set_env PG_PASSWORD "$PG_PASSWORD" + set_env SECRETS_SYSTEM "$SECRETS_SYSTEM" + set_env APP_VERSION "$APP_VERSION" + ok ".env angelegt, PG_PASSWORD + SECRETS_SYSTEM generiert (chmod 600)." +fi +# Host-Pfad des Deployments — braucht die Update-Funktion des Admin-UI (der +# Einweg-Updater mountet das Projektverzeichnis pfad-identisch). Bei jedem Lauf +# aktualisieren, falls das Verzeichnis umgezogen ist. +set_env PROJECT_DIR "$(pwd)" + +# --- 4. Updater-Image für die Admin-UI-Update-Funktion -------------------------- +info "Pulle Updater-Image ..." +if docker pull "$REGISTRY_IMAGE_BASE/sapb1mcp-updater:latest" >/dev/null; then + ok "Updater-Image bereit." +else + warn "Updater-Image konnte nicht gepullt werden — Registry-Zugriff prüfen; das Admin-UI-Update funktioniert sonst nicht." +fi + +# --- 5. Nächste Schritte --------------------------------------------------------- +cat < Port 4444) + LOGIN_URL öffentliche HTTPS-URL des Login-Dienstes (Reverse Proxy -> Port 8081) + MCP_RESOURCE_URL öffentliche HTTPS-URL des MCP-Servers (Reverse Proxy -> Port 8080) + SL_BASE_URL SAP Service Layer, z. B. https://sap-host:50000/b1s/ (trailing /b1s/ ist Pflicht) + SL_COMPANYDB SAP-Firmendatenbank + ADMIN_SAP_USER SAP-Benutzer, der das Admin-UI öffnen darf + MCP_ALLOWED_USERS kommagetrennte SAP-Benutzer mit MCP-Dauerzugriff + + Optional: SL_INSECURE (self-signed SAP-Zertifikat), BIND_ADDR/ADMIN_BIND und + *_HOST_PORT, falls der Reverse Proxy auf einem anderen Host läuft. + +Danach den Stack starten: + + ${COMPOSE} pull + ${COMPOSE} up -d + +Status prüfen: ${COMPOSE} ps (alle Dauerläufer müssen "healthy" werden) +Updates später: im Admin-UI unter „Update" — oder erneut 'bash setup.sh [version]' +gefolgt von '${COMPOSE} pull' + '${COMPOSE} up -d'. Details: INSTALL.md +============================================================================ +EOF