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

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 .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:

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:

  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:

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_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:

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

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.