# Das Sicherheitsmodell einer ERP-Integration

> Schlüssel und Scopes, Geheimnisse und Rotation, Transport, sichere Wiederholungen, Outbox, dünne Ereignisse, veraltete Schreibvorgänge, Datenminimierung, Audit-Trail und die OWASP API Top 10.

Eine Integration zwischen Ihrem ERP und BatteryPass bewegt Daten, die in signierten, öffentlichen Batteriepässen
landen. Diese Seite sammelt die Regeln, die diesen Weg sicher machen. Die meisten liegen auf Ihrer Seite der
Verbindung; wo BatteryPass etwas selbst durchsetzt, steht es dabei. Den Umgang mit Schlüsseln im Alltag beschreibt
[Schlüssel sicher verwalten](https://passport.kabin.sh/docs/de/guides/secure-key-handling).

## Schlüssel und Scopes

**Ein Schlüssel je Integration und je Umgebung.** Eine Test-Middleware und die produktive Middleware bekommen
verschiedene Schlüssel, ebenso zwei Integrationen in derselben Middleware. So können Sie einen widerrufen, ohne
die anderen anzuhalten, und die Spalte **Zuletzt verwendet** im Portal zeigt, welcher aktiv ist.

**So wenig Scope wie möglich.** Ein Schlüssel hat einen oder mehrere von drei Scopes:

| Scope       | Erlaubt dem Schlüssel                                                      | Abläufe                                                                                                                                                                                                                                       |
| ----------- | -------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `read`      | Modelle, Einheiten, Werte, Pässe, Exporte und den Änderungsfeed lesen      | [Pass-Links zurück ins ERP](https://passport.kabin.sh/docs/de/integrations/workflows/passport-links-to-erp), [Produktänderung](https://passport.kabin.sh/docs/de/integrations/workflows/product-change), Abfragen in den übrigen Abläufen |
| `condition` | Gemessene Zustandsdaten und Lebenszyklus-Ereignisse melden                 | [Zustandsdaten](https://passport.kabin.sh/docs/de/integrations/workflows/condition-from-bms), [Rücksendungen und Reparaturen](https://passport.kabin.sh/docs/de/integrations/workflows/returns-and-repairs)                               |
| `import`    | Tabellenimporte von Einheiten hochladen, zuordnen, prüfen lassen und lesen | [Einheiten aus der Fertigung](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production)                                                                                                                               |

Kein Scope erlaubt es, einen Import zu übernehmen, einen Wert freizugeben, zu veröffentlichen, zurückzuziehen,
Mitglieder oder Schlüssel zu verwalten oder deklarierte Daten zu ändern. Das bleibt bei Mitgliedern Ihrer
Organisation im Portal. Ein Schlüssel handelt mit den Rechten eines Betrachters plus seinen Scopes und ist nie auf
einzelne Einheiten beschränkt: Ein `condition`-Schlüssel kann für jede Einheit Ihrer Organisation melden.

## Geheimnisse

- Bewahren Sie jeden Schlüssel in einem Secrets-Manager oder Vault auf (HashiCorp Vault, Azure Key Vault, AWS
  Secrets Manager, der Anmeldedatenspeicher der iPaaS) und übergeben Sie ihn dem Prozess beim Start. Nie in Code,
  versionierte Konfiguration, Container-Images, URLs, Logs oder Tickets. Das
  [OWASP Secrets Management Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html)
  beschreibt die Praxis im Detail.
- Senden Sie den Schlüssel nur im Header `Authorization: Bearer bp_live_…`. Eine Anfrage, die zusätzlich ein
  Sitzungs-Cookie des Portals trägt, wird abgelehnt; so ist nie unklar, wer handelt.
- Maskieren Sie den Header `Authorization` in jedem Log Ihrer Middleware und Ihres HTTP-Clients.

**Rotation.** Schlüssel laufen nicht ab. Rotieren Sie nach eigenem Plan und immer dann, wenn jemand ausscheidet,
der einen Schlüssel kannte: neuen Schlüssel mit denselben Scopes anlegen, die Middleware darauf umstellen, prüfen,
dass der neue Schlüssel eine aktuelle Zeit unter **Zuletzt verwendet** zeigt, dann den alten widerrufen. Während
der Umstellung funktionieren beide, es fällt also nichts aus. Der Widerruf wirkt sofort und endgültig.

## Transport

- Rufen Sie BatteryPass nur über HTTPS (TLS) auf und prüfen Sie das Serverzertifikat. Schalten Sie die Prüfung
  auch in Testumgebungen nicht ab: Middleware-Konfigurationen wandern gern von Test nach Produktion.
- **Egress-Allowlisting** als zweite Linie: Erlauben Sie der Middleware nur die Adresse von BatteryPass und das
  ERP. Das ersetzt weder Schlüssel noch TLS, begrenzt aber, mit wem eine kompromittierte Middleware sprechen kann.
- Umgekehrt genauso: Machen Sie die APIs des ERP nur der Middleware zugänglich, nie direkt aus dem Internet.

## Wiederholungen ohne Duplikate

Netze fallen aus. Eine Anfrage mit Zeitüberschreitung wurde vielleicht ausgeführt, vielleicht nicht; Wiederholungen
müssen also sicher sein.

**Zurückhaltend wiederholen.** Wiederholen Sie Netzfehler und `5xx`-Antworten mit exponentiellem Backoff und
Jitter (etwa 1, 2, 4, 8 Sekunden plus ein Zufallsanteil, höchstens fünf Versuche). Auf `429 rate_limited` warten
Sie die Sekunden aus dem Header `Retry-After` ab; alle Schlüssel Ihrer Organisation teilen sich 600 Anfragen pro
Minute. Andere `4xx`-Antworten wiederholen Sie nicht: Die Anfrage wurde als Ganzes abgelehnt, korrigieren Sie sie.
Einzelheiten stehen in [Fehler und Wiederholungen behandeln](https://passport.kabin.sh/docs/de/guides/handle-errors) und
[Ratenlimits](https://passport.kabin.sh/docs/de/rate-limits).

**Jede Wiederholung sicher machen.** Einen Header `Idempotency-Key` nimmt BatteryPass nur bei Sammel- und
Veröffentlichungsoperationen des Portals an, die Schlüssel nicht aufrufen können. Bei den Operationen eines
Schlüssels kommt die Sicherheit aus den Operationen selbst und aus Ihrer Middleware:

| Operation                                                                                                    | Wiederholbar                    | Warum, oder was Sie tun                                                                                                                                                                                                                                                                                                  |
| ------------------------------------------------------------------------------------------------------------ | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Jedes `GET` (Lesen, Änderungsfeed, `getImport`)                                                              | Ja                              | Sie ändern nichts.                                                                                                                                                                                                                                                                                                       |
| [`updateCondition`](https://passport.kabin.sh/docs/de/api-reference/lifecycle/update-condition)            | Ja, mit demselben `measured_at` | Eine gleich alte oder ältere Messung als die gespeicherte wird übersprungen.                                                                                                                                                                                                                                             |
| [`createImport`](https://passport.kabin.sh/docs/de/api-reference/imports/create-import)                    | Ja, mit Bedacht                 | Ein wiederholter Upload legt einen zweiten Import an. Seine Zeilen werden zu Duplikaten, sobald der erste übernommen ist, sodass keine Einheit doppelt entsteht; Mitglieder übernehmen den überzähligen Import einfach nicht.                                                                                            |
| [`updateImportMapping`](https://passport.kabin.sh/docs/de/api-reference/imports/update-import-mapping)     | Ja                              | Mit demselben `If-Match` wird die Wiederholung als veraltet abgelehnt (`409`); lesen Sie den Import, sehen Sie die Zuordnung bereits angewandt.                                                                                                                                                                          |
| [`validateImport`](https://passport.kabin.sh/docs/de/api-reference/imports/validate-import)                | Ja                              | Ein zweiter Start, während die erste Prüfung läuft, wird abgelehnt (`409`); fragen Sie stattdessen den Import ab.                                                                                                                                                                                                        |
| [`createLifecycleEvent`](https://passport.kabin.sh/docs/de/api-reference/lifecycle/create-lifecycle-event) | Nein                            | Jeder Aufruf legt ein neues Ereignis an. Merken Sie sich, welchen Quellsatz (etwa den Serviceauftrag) Sie schon gemeldet haben, und sehen Sie nach einer Zeitüberschreitung in [`listLifecycleEvents`](https://passport.kabin.sh/docs/de/api-reference/lifecycle/list-lifecycle-events) nach, bevor Sie erneut senden. |

Die IETF arbeitet an einem allgemeinen Header `Idempotency-Key`
([draft-ietf-httpapi-idempotency-key-header](https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/),
Entwurf 08, noch kein RFC). Für Ihre eigenen APIs ist das ein nützlicher Hintergrund; BatteryPass nimmt ihn bei
den Operationen oben nicht an.

## Die Outbox auf ERP-Seite

Das ERP darf keine Änderung verlieren, während die Middleware oder BatteryPass nicht erreichbar ist, und es darf
BatteryPass nicht innerhalb seiner eigenen Geschäftstransaktion aufrufen. Die **transaktionale Outbox** löst
beides: In derselben Datenbanktransaktion, die den Wareneingang bucht, schreibt das ERP (oder eine Erweiterung)
eine Zeile in eine Ausgangstabelle. Die Middleware liest die Outbox, ruft BatteryPass auf und markiert die Zeile
erst nach einer erfolgreichen Antwort als erledigt.

Das ergibt **Zustellung mindestens einmal**: Eine Zeile kann zweimal gesendet werden (bei einem Absturz zwischen
Aufruf und Markierung), nie keinmal. Kombinieren Sie das mit den Regeln oben und führen Sie eine kleine Tabelle
dessen, was Sie schon gesendet haben (Quellsatz, BatteryPass-ID, Zeitpunkt), damit Sie auf Ihrer Seite
Duplikate erkennen.

## Dünne Ereignisse, dann abholen

Bewegen Sie keine Daten in Benachrichtigungen. Beide Richtungen funktionieren gleich:

- Vom ERP: Viele ERP-Systeme senden dünne Ereignisse (eine Produktnummer und die Art der Änderung). Die Middleware
  liest danach den aktuellen Satz über die API des ERP, prüft Signatur, Handshake oder geheime URL des Herstellers
  und verwirft Ereignisse, die sie nicht prüfen kann.
- Von BatteryPass: Der [Änderungsfeed](https://passport.kabin.sh/docs/de/api-reference/changes/list-changes) nennt nur
  `{id, occurred_at, entity_type, entity_id, action}`, nie Werte, Handelnde oder personenbezogene Daten. Die
  Middleware liest die Entität danach mit demselben Schlüssel; der Feed verrät für sich allein also nichts und ist
  immer aktuell.

Der Feed liefert eine Änderung erst, wenn jede ältere Transaktion in BatteryPass beendet ist; ein gespeicherter
Cursor überspringt also nie eine Änderung, die spät festgeschrieben wurde. Speichern Sie den Cursor erst, wenn Ihre
Seite die Seite verarbeitet hat. Ausgehende Webhooks sendet BatteryPass noch nicht; sie sind geplant und brauchen
eine eigene Entscheidung (Signatur, Verwaltung der Abonnements, Schutz der Zieladressen). Bis dahin fragen Sie ab.

## Veraltete Schreibvorgänge

Änderbare Ressourcen tragen eine `row_version`. [`updateImportMapping`](https://passport.kabin.sh/docs/de/api-reference/imports/update-import-mapping)
verlangt `If-Match: "<row_version>"`; hat jemand anderes den Import inzwischen geändert, lautet die Antwort
`409 conflict`, und nichts wird überschrieben. Lesen Sie den Import neu und entscheiden Sie. Zustandsdaten haben
eine eigene Regel: Eine ältere Messung ersetzt nie eine neuere.

## Datenminimierung

Die DSGVO verlangt Daten, die „dem Zweck angemessen und erheblich sowie auf das notwendige Maß beschränkt“ sind
(Art. 5 Abs. 1 lit. c). Für einen Batteriepass heißt das: Daten über die Batterie, also Seriennummer, Modell,
Daten, Messwerte, Ereignisse. Namen und Adressen von Kundinnen und Kunden, Auftragsnummern, Namen von
Beschäftigten, Preise, Fahrzeug- und Standortdaten bleiben im ERP. Jeder
[Ablauf](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production) nennt, was er sendet und was er weglässt; der
[Überblick](https://passport.kabin.sh/docs/de/integrations) nennt, was nie fließt.

Freitext ist die Stelle, an der personenbezogene Daten hineinrutschen: Eine Ereignisbeschreibung wie „repariert
für Herrn X in Y“ landet in der Historie Ihrer Batterie. Füllen Sie Beschreibungen aus festen Codes oder
Vorlagen, nicht aus dem Freitext eines Serviceauftrags.

## Der Audit-Trail

- Jeder Schreibvorgang mit einem Schlüssel steht im Audit-Log Ihrer Organisation, mit dem Handelnden-Typ
  `api_key` und dem Namen des Schlüssels: Bereitstellen und Prüfen eines Imports, eine Zustandsmeldung, ein
  Lebenszyklus-Ereignis.
- Importe, die ein Schlüssel bereitgestellt hat, nennen den Schlüssel statt eines Mitglieds
  (`created_by_api_key`), in der API und im Portal.
- Werte, die ein Schlüssel schreibt, tragen den Schlüssel nicht selbst. Eine Zustandsmeldung legt ein
  Lebenszyklus-Ereignis `condition_update` an, und dieses Ereignis nennt den Schlüssel (`api_key`).
- Zustandsdaten werden als freigegebene Messungen ohne Prüfer gespeichert. Deklarierte Daten nehmen den geprüften
  Weg: Importierte Werte gibt ein Prüfer frei, und ob eine zweite Person freigeben muss, legt die
  Vier-Augen-Einstellung Ihrer Organisation fest.
- Lesezugriffe werden nicht festgehalten. Gehen Sie davon aus, dass die Daten eines abhandengekommenen Schlüssels
  gelesen wurden.

## OWASP API Security Top 10

Wie die Risiken der [OWASP API Security Top 10 (2023)](https://api-security.owasp.org/editions/2023/en/0x11-t10),
die für eine ERP-Integration am meisten zählen, behandelt werden, und was Ihr Teil bleibt:

| Risiko                                        | In BatteryPass                                                                                                                                                                                                       | Ihr Teil                                                                                                                                                                                             |
| --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| API1:2023 Broken Object Level Authorization   | Jede Abfrage ist auf die Organisation des Schlüssels gefiltert; die ID einer anderen Organisation ergibt `404`.                                                                                                      | Ein Schlüssel je angebundener Organisation; nie Mandanten in einem Middleware-Ablauf mischen.                                                                                                        |
| API4:2023 Unrestricted Resource Consumption   | 600 Anfragen pro Minute für alle Schlüssel einer Organisation, 30 Versuche je 15 Minuten für unbekannte Schlüssel, Request-Bodys bis 1 MiB, Uploads bis 10 MB und 10 000 Zeilen, Listen mit höchstens 200 Einträgen. | Bei `429` zurückhalten, mit `limit` und `cursor` blättern, Dateien in Stapeln senden.                                                                                                                |
| API5:2023 Broken Function Level Authorization | Jede Operation nennt ihren Scope (`x-api-key-scope`); ein Schlüssel ohne ihn erhält `403`, Operationen des Portals nehmen gar keine Schlüssel an (`401`).                                                            | Jedem Schlüssel nur die Scopes geben, die sein Ablauf nennt.                                                                                                                                         |
| API7:2023 Server Side Request Forgery         | BatteryPass ruft keine Adressen auf, die eine Integration liefert; Webhooks gibt es noch nicht.                                                                                                                      | Holt die Middleware URLs aus ERP-Ereignissen ab, die Ziele per Allowlist begrenzen.                                                                                                                  |
| API10:2023 Unsafe Consumption of APIs         | BatteryPass prüft jeden Wert nach den Regeln des Registers, gleich aus welcher Quelle.                                                                                                                               | ERP-Ereignisse und Antworten von BatteryPass als nicht vertrauenswürdige Eingaben behandeln: prüfen, Signaturen kontrollieren, wo der Hersteller sie anbietet, und empfangene Inhalte nie ausführen. |
