From d7e776b3de151fe28d41ee53b71215343cec09b4 Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Wed, 15 Oct 2025 12:33:46 +0200 Subject: [PATCH] admin tool doku --- .../appdoc/doc_admin_encrypt_env_secret.md | 131 ++++++++++++++++++ ...On NDA 2025.docx => PowerOn NDA 2026.docx} | Bin 15607 -> 15553 bytes 2 files changed, 131 insertions(+) create mode 100644 poweron/appdoc/doc_admin_encrypt_env_secret.md rename poweron/testdata-wait/{PowerOn NDA 2025.docx => PowerOn NDA 2026.docx} (51%) diff --git a/poweron/appdoc/doc_admin_encrypt_env_secret.md b/poweron/appdoc/doc_admin_encrypt_env_secret.md new file mode 100644 index 0000000..5be3eef --- /dev/null +++ b/poweron/appdoc/doc_admin_encrypt_env_secret.md @@ -0,0 +1,131 @@ +### Admin-Leitfaden: *_SECRET-Werte in Env-Dateien verschlüsseln + +Diese Anleitung beschreibt, wie das PowerOn-Gateway-Tool `tool_security_encrypt_all_env_files.py` alle unverschlüsselten `*_SECRET`-Variablen in `env_dev.env`, `env_int.env` und `env_prod.env` verschlüsselt. + +--- + +### 1) Einsatz in beliebigen Repositories & Voraussetzungen + +Das Tool kann in jedem Repository eingesetzt werden, sofern die untenstehenden Vorbedingungen erfüllt sind. Standardmäßig ist es im Ordner `gateway` integriert und erwartet dort seine Abhängigkeiten. In anderen Repos müssen Sie die Gateway-Komponenten bereitstellen oder den `PYTHONPATH` entsprechend setzen. + +- **Ausführungspfad**: Entweder im Verzeichnis, das die Gateway-Module `modules/shared` enthält (z. B. `gateway`), oder mit gesetztem `PYTHONPATH`, sodass `modules/shared/configuration.py` importiert werden kann. +- **Python**: Python 3.10+ mit Abhängigkeiten (siehe Kapitel „Vorbedingungen“). Installation z. B.: + - Windows PowerShell im Repo-Root: `python -m venv .venv && .\.venv\Scripts\Activate.ps1` + - `pip install -r requirements.txt` +- **Dateien**: + - Eine `config.ini` (im gleichen Projektteil wie die `modules`), die zumindest `APP_ENV_TYPE` und `APP_KEY_SYSVAR` enthält. + - Die zu verarbeitenden Env-Dateien: `env_dev.env`, `env_int.env`, `env_prod.env` (oder eine Teilmenge via `--files`). +- **Rechte**: Schreibrechte auf die Env-Dateien, Leserechte auf die Masterkey-Quelle (siehe Punkt 2). + +Hinweis: Das Tool importiert `encrypt_value` aus `modules/shared/configuration.py`. Stellen Sie sicher, dass dieser Pfad importierbar ist (Start im Gateway-Verzeichnis oder `PYTHONPATH` setzen). + +--- + +### Vorbedingungen + +1. `requirements.txt` im Projekt (Minimalbeispiel): + ``` + cryptography>=42.0.0 + ``` + Falls Sie das Tool im bestehenden Gateway verwenden, nutzen Sie stattdessen die dortige Datei `gateway/requirements.txt`. + +2. Notwendige Gateway-Komponenten (sofern außerhalb von `gateway` verwendet): + - `modules/shared/configuration.py` (liefert `APP_CONFIG`, `_get_master_key`, `encrypt_value`, `decrypt_value`) + - Optional, aber unterstützt: `modules/shared/auditLogger.py` (Audit-Events; Ausfall ist toleriert) + - `config.ini` im gleichen Baum wie `modules`, mit mindestens: + - `APP_ENV_TYPE = dev|int|prod` (kann pro Env-Datei zusätzlich gesetzt werden) + - `APP_KEY_SYSVAR = ` + +3. Struktur/Importpfade sicherstellen: + - Entweder das Tool im Ordner ausführen, der die `modules`-Struktur enthält (z. B. `gateway`). + - Oder `PYTHONPATH` so setzen, dass `modules` auflösbar ist, z. B. PowerShell: + ```powershell + $env:PYTHONPATH = (Resolve-Path .) + python tool_security_encrypt_all_env_files.py --dry-run + ``` + +--- + +### 2) Masterkey: Herkunft pro Umgebung + +Die Verschlüsselung verwendet pro Umgebung einen Masterkey, der in `modules/shared/configuration.py` über `_get_master_key(env_type)` ermittelt wird. Quelle des Keys: + +1. `APP_KEY_SYSVAR` (aus `config.ini` oder `.env`) gibt entweder + - den **Namen einer OS-Umgebungsvariablen** an, die den Masterkey direkt enthält, oder + - einen **Dateipfad** zu einer Key-Datei an. + +2. Auflösung in dieser Reihenfolge: + - Wenn eine OS-Umgebungsvariable mit dem Namen `APP_KEY_SYSVAR` existiert, wird deren Wert direkt als Masterkey verwendet. + - Wenn keine OS-Variable gefunden wird, wird der Wert als Pfad interpretiert. Existiert die Datei, wird sie gelesen. Erwartetes Format je Zeile: `env = key`, z. B.: + - `dev = ` + - `int = ` + - `prod = ` + Für die Zielumgebung (z. B. `prod`) wird die passend benannte Zeile verwendet. + +3. Der abgeleitete Key wird per PBKDF2 in einen 32-Byte-Schlüssel umgewandelt und für Fernet genutzt. Der verschlüsselte Wert erhält ein Präfix gemäß Umgebung: `DEV_ENC:`, `INT_ENC:`, `PROD_ENC:` (weitere unterstützt: `TEST_ENC:`, `STAGING_ENC:`). + +Wichtig: Die Umgebung für die Verschlüsselung wird pro Datei aus der jeweiligen Datei selbst gelesen (`APP_ENV_TYPE=`). D. h. `env_prod.env` wird mit `prod`-Key verschlüsselt, unabhängig von der aktuellen Prozess-Umgebung. + +--- + +### 3) Anwendung und Verhalten + +Tool: `gateway/tool_security_encrypt_all_env_files.py` + +- Standardlauf (alle drei Dateien, mit Backup): + ```bash + python tool_security_encrypt_all_env_files.py + ``` + Wirkung: + - Liest je Datei `APP_ENV_TYPE`. + - Findet alle Schlüssel, deren Name auf `_SECRET` endet, und deren Werte noch kein `*_ENC:`-Präfix haben. + - Unterstützt einzeilige und mehrzeilige JSON-Werte (Klammer-Auf/Zu wird erkannt). + - Verschlüsselt die Werte mit dem passenden Umgebungs-Key und ersetzt die Originalwerte in der Datei durch `ENV_ENC:`. + - Legt vor Änderungen eine Backup-Datei mit Zeitstempel neben der Originaldatei an, z. B. `env_prod.env.20251015_101530.backup`. + +- Trockenlauf (nur anzeigen, nichts schreiben): + ```bash + python tool_security_encrypt_all_env_files.py --dry-run + ``` + Wirkung: Listet, welche Zeilen/Keys verschlüsselt würden. Es werden keine Änderungen vorgenommen und keine Backups erstellt. + +- Ohne Backups ausführen: + ```bash + python tool_security_encrypt_all_env_files.py --no-backup + ``` + Wirkung: Schreibt Änderungen ohne vorherige Backup-Datei. + +- Nur bestimmte Dateien verarbeiten: + ```bash + python tool_security_encrypt_all_env_files.py --files env_dev.env env_prod.env + ``` + +Ausgabe/Ergebnis: +- Pro Datei: Anzahl gefundener Secrets, Anzahl verschlüsselter Secrets, ggf. Backup-Datei, Fehler. +- Zusammenfassung am Ende über alle verarbeiteten Dateien. + +Was wird als „bereits verschlüsselt“ erkannt? +- Alle Werte, die mit einem der Präfixe beginnen: `DEV_ENC:`, `INT_ENC:`, `PROD_ENC:`, `TEST_ENC:`, `STAGING_ENC:`. Diese werden übersprungen. + +Fehlerbilder und Checks: +- Fehlt `APP_KEY_SYSVAR` in `config.ini`/`.env` oder zeigt er auf eine nicht existierende OS-Variable/Datei, schlägt die Verschlüsselung fehl. +- Fehlt in einer Key-Datei der passende Eintrag für die Zielumgebung, wird ein Fehler gemeldet. +- Schreibfehler (z. B. fehlende Rechte) verhindern das Aktualisieren der Env-Datei. + +--- + +### Best Practices + +- Änderungen zuerst mit `--dry-run` prüfen. +- Vor Produktivläufen sicherstellen, dass `APP_KEY_SYSVAR` korrekt auf OS-Variable oder Key-Datei zeigt und die Datei einen Eintrag für die Zielumgebung enthält. +- Backups beibehalten, um bei Bedarf Änderungen zurückzurollen. +- JSON-Secrets in Env-Dateien korrekt mit `{ ... }` kapseln; das Tool erkennt und ersetzt mehrzeilige JSON-Blöcke vollständig. + +--- + +### Referenz + +- Schlüsselauflösung und Verschlüsselung: `gateway/modules/shared/configuration.py` (`_get_master_key`, `_derive_encryption_key`, `encrypt_value`). +- Batch-Tool: `gateway/tool_security_encrypt_all_env_files.py` (`encrypt_all_secrets_in_file`, `process_all_env_files`). + + diff --git a/poweron/testdata-wait/PowerOn NDA 2025.docx b/poweron/testdata-wait/PowerOn NDA 2026.docx similarity index 51% rename from poweron/testdata-wait/PowerOn NDA 2025.docx rename to poweron/testdata-wait/PowerOn NDA 2026.docx index 66ea0b504e2bb53407e38042f9f4a8f1b8c68a22..364fb2646efcca46779b5365ac293fbc49f6eade 100644 GIT binary patch delta 5190 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