Files
jbergner fe32431d21
Some checks failed
release-tag / release-image (push) Failing after 55s
init
2026-08-01 23:54:45 +02:00

317 lines
12 KiB
Markdown

# 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/<projekt>
└── Docker CLI
├── docker restart <allowlisted-name>
└── docker compose ... up -d --no-deps --force-recreate <service>
```
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.