release 2026.07.07-24520d4
This commit is contained in:
@@ -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).
|
||||
+105
@@ -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://<sap-host>: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 <version>` 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.<domain>` | 8080 | MCP-Resource-Server |
|
||||
| `mcp-oauth.<domain>` | 4444 | Hydra (OAuth-Issuer) |
|
||||
| `mcp-login.<domain>` | 8081 | Login/Consent |
|
||||
| `mcp-admin.<domain>` (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 <version>`).
|
||||
|
||||
## 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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 <version> # 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 <<EOF
|
||||
|
||||
============================================================================
|
||||
Setup abgeschlossen. Bitte jetzt in der .env die Umgebungswerte anpassen:
|
||||
|
||||
PUBLIC_ISSUER öffentliche HTTPS-URL des OAuth-Issuers (Reverse Proxy -> 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
|
||||
Reference in New Issue
Block a user