12 KiB
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
.envund 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
0600ab - 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
restartund Compose-basiertesrecreate - 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:
- Pfad aus
-config - Pfad aus
ENV_CONTROLLER_CONFIG controller.jsonim aktuellen Arbeitsverzeichniscontroller.jsonneben der EXE- Container-Standard
/config/controller.json
Erstelle zunächst eine lokale Konfiguration:
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:
{
"password_env": "ENV_CONTROLLER_PASSWORD",
"password_file": ""
}
Setze anschließend das Passwort und starte die Anwendung:
$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:
$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
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
cp controller.password.example controller.password
cp controller.example.json controller.json
mkdir -p backups
Danach anpassen:
- den zufälligen Inhalt von
controller.password - Host-Pfade in
compose.yml - Projekte und erlaubte Container in
controller.json - stabile Container-Namen über ein Compose-Override festlegen
Start:
docker compose -f compose.yml up -d --build
Aufruf standardmäßig:
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:
services:
agent:
container_name: glpi-ai-agent
ollama:
container_name: glpi-ai-ollama
Das Projekt muss anschließend mit beiden Dateien erstellt werden:
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:
{
"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
"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
{
"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äß:
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:
{
"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:
{
"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_dircom.docker.compose.project.config_filescom.docker.compose.projectcom.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:
editvor dem Speichernimportvor dem Import neuer Schlüsselpre_restorevor einer Wiederherstellungimportbeim automatischen Startimport
Beispiel:
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:
PASSWORDPASSWDSECRETTOKENAPI_KEYPRIVATE_KEYCREDENTIAL
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
restartund 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: ALLundno-new-privilegeslä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
.envnicht verändert. - Schlägt das atomare Schreiben fehl, bleibt die vorherige Datei bestehen.
- Schlägt ein Recreate nach erfolgreichem Speichern fehl, bleibt die neue
.envgespeichert 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 inspectnachzubauen.
Entwicklung
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.