# ENV Controller Eigenständige Webanwendung zum Vergleichen, Bearbeiten, Sichern und Wiederherstellen von `.env`-Dateien. Nach einer Änderung können ausschließlich freigegebene Docker-Container kontrolliert neu gestartet oder über Docker Compose neu erstellt werden. ## Kernfunktionen - liest eine bestehende `.env` und die zugehörige `.env.example` - zeigt fehlende und zusätzliche Schlüssel an - importiert neue Schlüssel aus `.env.example`, ohne bestehende Werte zu überschreiben - erhält Kommentare, Reihenfolge und zusätzliche lokale Schlüssel weitgehend unverändert - erstellt **vor jedem Import, Speichern und Restore** automatisch ein Backup - legt Backup-Dateien und Metadaten mit Dateimodus `0600` ab - ermöglicht Download und Wiederherstellung früherer Stände - lädt Secret-Werte erst beim Fokus des Feldes vom Server - erlaubt Docker-Aktionen nur für eine feste Allowlist exakter `container_name`-Werte - unterstützt `restart` und Compose-basiertes `recreate` - kann abhängige Dritt-Container aus anderen Compose-Projekten mit derselben Änderung neu erstellen - kann fehlende Schlüssel optional beim Controller-Start automatisch importieren ## Plattformkompatibilität Der Controller ist für den produktiven Betrieb als Linux-Container vorgesehen. Der Quellcode lässt sich zusätzlich unter Windows entwickeln und kompilieren. Unix-Dateibesitzrechte (UID/GID) werden nur auf Unix-Systemen übernommen; unter Windows wird dieser nicht verfügbare Schritt übersprungen. ## Direkter Start unter Windows Beim direkten Start der EXE wird die Konfiguration in dieser Reihenfolge gesucht: 1. Pfad aus `-config` 2. Pfad aus `ENV_CONTROLLER_CONFIG` 3. `controller.json` im aktuellen Arbeitsverzeichnis 4. `controller.json` neben der EXE 5. Container-Standard `/config/controller.json` Erstelle zunächst eine lokale Konfiguration: ```powershell Copy-Item .\controller.windows.example.json .\controller.json ``` Die mitgelieferte `controller.windows.example.json` verwendet bereits eine Passwort-Umgebungsvariable. Bei einer Übernahme aus `controller.example.json` müssen diese Felder so gesetzt werden: ```json { "password_env": "ENV_CONTROLLER_PASSWORD", "password_file": "" } ``` Setze anschließend das Passwort und starte die Anwendung: ```powershell $env:ENV_CONTROLLER_PASSWORD = "ein-langes-zufaelliges-passwort" .\dist\env-controller-windows-amd64.exe -config .\controller.json ``` Alternativ kann der Pfad dauerhaft über die Umgebung gesetzt werden: ```powershell $env:ENV_CONTROLLER_CONFIG = "E:\GoProjects\glpi-ai-agent-controller\controller.json" .\dist\env-controller-windows-amd64.exe ``` Die Projektpfade in `controller.json` müssen unter Windows als absolute Pfade angegeben werden, beispielsweise `E:/GoProjects/glpi-ai-agent/.env`. Der produktive Docker-Betrieb verwendet weiterhin `/config/controller.json` und das Compose-Secret. ## Architektur ```text Browser │ Basic Auth + CSRF ▼ ENV Controller ├── liest/schreibt /opt/.../.env atomar ├── vergleicht /opt/.../.env.example ├── sichert nach /backups/ └── Docker CLI ├── docker restart └── docker compose ... up -d --no-deps --force-recreate ``` Das Schreiben der `.env` verändert die bereits gestartete Prozessumgebung eines Containers nicht. Erst ein Compose-Recreate übernimmt die neue Konfiguration. Ein normaler Restart ist nur für Anwendungen geeignet, die ihre Konfiguration selbst aus einer gemounteten Datei neu einlesen. ## Schnellstart ```bash cp controller.password.example controller.password cp controller.example.json controller.json mkdir -p backups ``` Danach anpassen: 1. den zufälligen Inhalt von `controller.password` 2. Host-Pfade in `compose.yml` 3. Projekte und erlaubte Container in `controller.json` 4. stabile Container-Namen über ein Compose-Override festlegen Start: ```bash docker compose -f compose.yml up -d --build ``` Aufruf standardmäßig: ```text http://127.0.0.1:8090 ``` Benutzername ist im Beispiel `admin`; das Passwort wird als Compose-Secret aus `controller.password` eingebunden. Alternativ unterstützt die Anwendung weiterhin `password_env` in `controller.json`. ## Stabile Container-Namen Der Controller arbeitet absichtlich mit exakten Namen. Im verwalteten Projekt kann dafür ein Override wie `examples/compose.controller-names.yml` verwendet werden: ```yaml services: agent: container_name: glpi-ai-agent ollama: container_name: glpi-ai-ollama ``` Das Projekt muss anschließend mit beiden Dateien erstellt werden: ```bash docker compose \ -f docker-compose.yml \ -f compose.controller-names.yml \ up -d ``` Dieselben Compose-Dateien müssen beim Ziel in `controller.json` angegeben sein, damit ein Recreate die identische Definition verwendet. ## Projektkonfiguration Ein Projekt verwaltet genau eine `.env` samt `.env.example` und Backup-Verzeichnis: ```json { "id": "glpi-ai-stack", "title": "GLPI AI Stack", "env_file": "/opt/glpi-ai-stack/.env", "example_file": "/opt/glpi-ai-stack/.env.example", "backup_dir": "/backups/glpi-ai-stack", "targets": [] } ``` Alle drei Pfade müssen im Container absolut sein. Die `.env` und das Backup-Verzeichnis benötigen Schreibzugriff; `.env.example` darf read-only gemountet werden, sofern sie separat eingebunden wird. ### Automatischer Import ```json "auto_import_missing": true ``` Beim Start werden fehlende Schlüssel automatisch importiert. Auch dabei wird vorher ein Backup erstellt. Standardmäßig ist die Funktion deaktiviert, damit neue Vorlagenwerte zunächst geprüft werden können. ## Container-Ziele ### Compose-Recreate ```json { "container_name": "glpi-ai-agent", "display_name": "GLPI AI Agent", "allowed_actions": ["restart", "recreate"], "default_action": "recreate", "apply_by_default": true, "project_dir": "/opt/glpi-ai-stack", "compose_files": [ "/opt/glpi-ai-stack/docker-compose.yml", "/opt/glpi-ai-stack/compose.controller-names.yml" ], "compose_service": "agent", "compose_project": "glpi-ai-stack", "env_file": "/opt/glpi-ai-stack/.env" } ``` Ausgeführt wird sinngemäß: ```bash docker compose \ --env-file /opt/glpi-ai-stack/.env \ --project-directory /opt/glpi-ai-stack \ -p glpi-ai-stack \ -f /opt/glpi-ai-stack/docker-compose.yml \ -f /opt/glpi-ai-stack/compose.controller-names.yml \ up -d --no-deps --force-recreate agent ``` `--no-deps` verhindert, dass Compose eigenständig weitere Dienste verändert. Gewünschte abhängige Dienste werden als separate, explizite Ziele eingetragen und in der Oberfläche ausgewählt. ### Einfacher Restart Für einen nicht von Compose verwalteten Container oder eine Anwendung, die eine gemountete Datei bei Restart selbst liest: ```json { "container_name": "legacy-service", "display_name": "Legacy Service", "allowed_actions": ["restart"], "default_action": "restart", "apply_by_default": false } ``` ### Dritt-Container aus einem anderen Compose-Projekt Ein Ziel kann ein anderes `project_dir`, andere Compose-Dateien und einen anderen Service besitzen. Mit `env_file` kann dennoch die gerade verwaltete gemeinsame `.env` an Compose übergeben werden: ```json { "container_name": "glpi-kb-search", "allowed_actions": ["recreate"], "default_action": "recreate", "apply_by_default": true, "project_dir": "/opt/glpi-kb-stack", "compose_files": ["/opt/glpi-kb-stack/compose.yml"], "compose_service": "kb-search", "compose_project": "glpi-kb-stack", "env_file": "/opt/glpi-ai-stack/.env" } ``` Damit lassen sich mehrere von gemeinsamen Einstellungen abhängige Anwendungen gezielt nacheinander aktualisieren. ## Automatische Compose-Erkennung Fehlen die expliziten Felder `project_dir`, `compose_files` oder `compose_service`, versucht der Controller diese Werte aus folgenden Compose-Labels des laufenden Containers zu lesen: - `com.docker.compose.project.working_dir` - `com.docker.compose.project.config_files` - `com.docker.compose.project` - `com.docker.compose.service` Explizite Angaben sind robuster. Bei automatischer Erkennung müssen die im Label genannten Host-Pfade unter exakt demselben Pfad in den Controller gemountet sein. ## Backup-Verhalten Vor jeder Mutation wird der aktuell gültige Stand gesichert: - `edit` vor dem Speichern - `import` vor dem Import neuer Schlüssel - `pre_restore` vor einer Wiederherstellung - `import` beim automatischen Startimport Beispiel: ```text 20260801T211500.123456789Z_edit_f2c9148c7a11.env 20260801T211500.123456789Z_edit_f2c9148c7a11.env.json ``` Die JSON-Metadaten enthalten Zeitpunkt, Grund, Benutzer, Größe und SHA-256. Zusätzlich wird `audit.jsonl` fortgeschrieben. `max_backups` begrenzt die Anzahl der aufbewahrten `.env`-Revisionen je Projekt. Das Wiederherstellen erzeugt zuerst ein neues `pre_restore`-Backup des aktuellen Standes. Dadurch kann auch ein versehentlicher Restore unmittelbar rückgängig gemacht werden. ## Secret-Felder Ein Schlüssel gilt als geheim, wenn sein Name beispielsweise eines dieser Muster enthält: - `PASSWORD` - `PASSWD` - `SECRET` - `TOKEN` - `API_KEY` - `PRIVATE_KEY` - `CREDENTIAL` Der Klartext wird nicht in die initiale HTML-Seite eingebettet. Beim Fokus sendet der Browser einen authentifizierten, CSRF-geschützten Request. Beim Blur wird das Feld wieder als Passwortfeld dargestellt. Der Klartext befindet sich nach dem Öffnen technisch im Browserprozess; die Weboberfläche darf deshalb ausschließlich über ein vertrauenswürdiges Netz und vorzugsweise TLS erreichbar sein. ## Sicherheitsmodell Der Docker-Socket verleiht dem Controller sehr weitreichende Rechte auf dem Docker-Host. Die Anwendung reduziert die Angriffsfläche, indem sie: - keine frei eingegebenen Container-Namen akzeptiert - keine frei eingegebenen Shell-Befehle ausführt - Aktionen und Namen ausschließlich aus `controller.json` übernimmt - nur `restart` und einen fest aufgebauten Compose-Recreate unterstützt - Basic Auth und CSRF-Schutz verwendet - Sicherheitsheader setzt - Schreibvorgänge atomar ausführt - als read-only Container mit `cap_drop: ALL` und `no-new-privileges` läuft Der Socket bleibt trotzdem eine hochprivilegierte Schnittstelle. Die Oberfläche sollte nur an `127.0.0.1` oder ein internes Verwaltungsnetz gebunden und über einen TLS-Reverse-Proxy mit zusätzlicher Zugriffskontrolle veröffentlicht werden. ## Dateibesitz und Rechte Beim atomaren Ersetzen versucht der Controller Modus, UID und GID der ursprünglichen `.env` beizubehalten. Backups werden mit `0600` angelegt. Der Controller benötigt Schreibrechte auf: - das Verzeichnis der `.env` - alle `backup_dir`-Verzeichnisse Für den Docker-Socket läuft das Beispiel als root im Container. Dies bedeutet nicht, dass der Socket dadurch weniger privilegiert wäre; der Zugriff auf den Daemon selbst ist bereits die entscheidende Berechtigung. ## Verhalten bei Fehlern - Schlägt das Backup fehl, wird die `.env` nicht verändert. - Schlägt das atomare Schreiben fehl, bleibt die vorherige Datei bestehen. - Schlägt ein Recreate nach erfolgreichem Speichern fehl, bleibt die neue `.env` gespeichert und die Oberfläche zeigt den Fehler je Ziel an. Das vorherige Backup kann wiederhergestellt werden. - Nicht allowlistete Namen oder Aktionen werden abgelehnt. - Ein Recreate ohne vollständige Compose-Metadaten wird abgelehnt, statt einen Container improvisiert aus `docker inspect` nachzubauen. ## Entwicklung ```bash make test make vet make race make build ``` Das Projekt verwendet ausschließlich die Go-Standardbibliothek. Für Container-Aktionen wird die Docker CLI mit Compose-Plugin im Image verwendet.