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