# NetBox Plugin Store Der Store ist eine eigenständige PHP-Anwendung für einen kuratierten NetBox-Plugin-Katalog. Er liest Repositories von Forgejo/Gitea (standardmäßig `git.mrblake.cc`) ein, erkennt Plugin-Metadaten, rendert die README der exakt synchronisierten Commit-Revision und veröffentlicht erst nach einer getrennten Admin-Freigabe einen API-v1-Katalog. Die native Installation mit PHP und Apache ist der primäre Betriebsweg. Docker ist optional. ## Funktionsumfang - responsive deutschsprachige Store-, Such-, Filter- und Plugin-Detailseiten; - vollständige Forgejo-/Gitea- und GitHub-Discovery für Benutzer und Organisationen; - Erkennung über Topic, `netbox-plugin.json|yaml`, `pyproject.toml`, `setup.py` und `PluginConfig`; - commitgenaue Reads für Manifest, Projektmetadaten, Tree, PluginConfig und README; - CommonMark-Rendering ohne eingebettetes HTML; relative Links und Bilder werden auf die Raw-URL des Commit-SHA umgeschrieben; - eigener Admin-Login mit Argon2id, CSRF-Schutz, Session-Härtung und persistentem Login-Rate-Limit; - Freigabe/Ablehnung von Quellen, Plugins und jedem Release-Artefakt; Admin-Metadatenkorrekturen und Audit-Log; - tatsächlicher Download jedes öffentlichen Wheel-Artefakts mit SHA-256 und Größe; sicherheitsrelevante Änderungen setzen die Freigabe zurück; - öffentlicher, freigegebener JSON-Katalog unter `/api/v1/`; - atomar geschriebener JSON-Datastore als Zero-Setup-Standard oder optional MariaDB über PDO; - einmaliger Sync sowie signalverträglicher Watch-Modus ohne Cron/Celery; - pro Source eine prozessübergreifende exklusive Sync-Lease sowie aggregierte Limits für Laufzeit, Requests, Bytes, Repositories und Releases. Wichtig: Ein eingelesenes und freigegebenes Repository ist noch nicht automatisch installierbar. API v1 veröffentlicht ausschließlich separat freigegebene `.whl`-Artefakte. Fehlt ein Wheel, bleibt das Plugin als Katalogeintrag sichtbar und `latest_version` ist `null`; die UI zeigt „Kein installierbares Release“. ## 1. Native Installation (empfohlen) ### Voraussetzungen - PHP 8.3 oder neuer (getestet mit PHP 8.4); - Apache 2.4 mit `mod_rewrite` und `mod_headers`; - Composer 2; - PHP-Erweiterungen: `curl`, `dom`, `iconv`, `intl`, `json`, `mbstring`, `pdo` und für MariaDB zusätzlich `pdo_mysql`; für einen graceful beendbaren dauerhaften `--watch`-Prozess wird `pcntl` empfohlen; - Schreibzugriff des Apache-/Sync-Benutzers auf `store/data`. Beispiel für Debian/Ubuntu (Paketnamen können je Distribution/PHP-Repository abweichen): ```bash sudo apt update sudo apt install apache2 libapache2-mod-php php-cli php-curl php-xml php-intl php-mbstring php-mysql composer sudo a2enmod rewrite headers ``` ### Anwendung installieren ```bash sudo mkdir -p /opt/netbox-plugin-store sudo chown "$USER":"$USER" /opt/netbox-plugin-store git clone /opt/netbox-plugin-store cd /opt/netbox-plugin-store/store composer install --no-dev --no-interaction --prefer-dist --classmap-authoritative cp .env.example .env mkdir -p data sudo chown -R www-data:www-data data sudo chmod 750 data ``` Der JSON-Pfad in `.env.example` ist relativ zum `store/`-Verzeichnis und funktioniert nativ ohne Änderung: ```dotenv STORE_DB_DRIVER=json STORE_JSON_PATH=./data/store.json ``` Der Datastore verwendet einen prozessübergreifenden `flock`, schreibt in eine temporäre Datei, synchronisiert sie und ersetzt anschließend die alte Datei atomar. Eine separate nicht-blockierende Lease-Datei hält zusätzlich jeden Source-Sync für seine gesamte Laufzeit exklusiv; ein Prozessabbruch gibt die Kernel-Sperre frei. Verwende ein lokales Dateisystem; geteilte/NFS-Dateisysteme bieten nicht immer die benötigten Lock-/Rename-Garantien. Für mehrere Web-Hosts ist MariaDB die bessere Wahl; dort übernimmt `GET_LOCK` die Sync-Lease. ### Admin-Zugang sicher konfigurieren Es gibt absichtlich keine Standard-Zugangsdaten. Erzeuge einen langen Zufallswert als Session-Secret und einen Argon2id-Hash: ```bash php -r 'echo bin2hex(random_bytes(32)), PHP_EOL;' php bin/console hash-password 'ein-langes-zufaelliges-passwort' ``` Trage Benutzername, ausgegebenen Hash und Secret vollständig in `.env` ein: ```dotenv STORE_ADMIN_USERNAME=admin STORE_ADMIN_PASSWORD_HASH=$argon2id$... STORE_SESSION_SECRET= ``` Sind alle drei Werte leer, bleibt `/admin` deaktiviert. Ist nur ein Teil gesetzt, bricht die Anwendung fail-closed mit einer Konfigurationsmeldung ab. Vermeide Passwörter in der Shell-History; alternativ liest `hash-password` den Wert aus `STORE_ADMIN_PASSWORD_TO_HASH`. Setze außerdem die öffentliche URL. Sichere Cookies richten sich standardmäßig nach ihrem Protokoll: ```dotenv APP_ENV=production STORE_PUBLIC_URL=https://plugins.example.com # Optionaler expliziter Override: STORE_COOKIE_SECURE=true ``` Ein Production-Quickstart über `http://localhost` funktioniert mit `STORE_PUBLIC_URL=http://localhost:3000` und `STORE_COOKIE_SECURE=false`; für einen öffentlichen Betrieb ist HTTPS erforderlich. ### Apache konfigurieren Nur `store/public` darf DocumentRoot sein. Passe Pfad und Servernamen in [deploy/apache-vhost.conf.example](deploy/apache-vhost.conf.example) an: ```bash sudo cp deploy/apache-vhost.conf.example /etc/apache2/sites-available/netbox-plugin-store.conf sudo editor /etc/apache2/sites-available/netbox-plugin-store.conf sudo a2ensite netbox-plugin-store sudo apache2ctl configtest sudo systemctl reload apache2 ``` Die Beispielkonfiguration erlaubt `.htaccess` ausschließlich im Store-DocumentRoot. Alternativ können Rewrite- und Header-Regeln aus `public/.htaccess` direkt in den vHost übernommen und `AllowOverride None` gesetzt werden. Für Produktion HTTPS direkt mit Apache `mod_ssl`/ACME konfigurieren und `STORE_PUBLIC_URL` auf `https://…` setzen; ein Reverse Proxy ist nicht erforderlich. Übernimm für den produktiven PHP-SAPI außerdem die sicherheitsrelevanten Werte aus `deploy/php-production.ini` (insbesondere `display_errors=Off`, `log_errors=On` und `expose_php=Off`) in deine PHP-Konfiguration und lade Apache neu. Der Front Controller deaktiviert die Fehlerausgabe zusätzlich selbst. ### Bootstrap und erster Sync ```bash cd /opt/netbox-plugin-store/store sudo -u www-data php bin/console bootstrap sudo -u www-data php bin/console sync ``` Wenn noch keine Quelle existiert, legt `bootstrap` (und standardmäßig auch `sync`) aus den `STORE_DEFAULT_*`-Werten eine aktive, freigegebene Forgejo-Quelle an. Standardwerte: - Provider `forgejo`; - Basis `https://git.mrblake.cc`; - API `https://git.mrblake.cc/api/v1`; - Benutzer `MrBlake` – entsprechend `/api/v1/users/MrBlake/repos`. Mit `--no-bootstrap` wird die automatische Anlage unterdrückt. Einzelne Quellen lassen sich mit `--source=slug` synchronisieren; `--fail-fast` beendet den Lauf beim ersten Repositoryfehler. Jeder Source-Lauf ist mit konservativen, konfigurierbaren Gesamtbudgets begrenzt. Eine Überschreitung beendet ihn fail-closed als `failed`; neue Freigaben entstehen dabei nicht. Die Defaults stehen auch in `.env.example`: ```dotenv STORE_SYNC_MAX_SECONDS=900 STORE_SYNC_MAX_REQUESTS=2500 STORE_SYNC_MAX_BYTES=1073741824 STORE_SYNC_MAX_REPOSITORIES=2000 STORE_SYNC_MAX_RELEASES=1000 ``` Die Limits zählen Redirects und Retries als weitere Requests sowie Metadaten und Artefakte gemeinsam gegen das Byte-Budget. Repository- und Release-Zähler werden nach jeder validierten Provider-API-Seite vor deren Übernahme in den Akkumulator geprüft; dadurch kann höchstens die gerade empfangene Seite zusätzlich im Speicher liegen. `STORE_SYNC_MAX_RELEASES` ist zugleich auf die API-v1-Grenze von 1.000 begrenzt. Ein zweiter Lauf derselben Source wird unabhängig vom Alter eines sichtbaren Run-Eintrags abgewiesen; nach einem Prozessabbruch wird der verwaiste Eintrag erst nach erfolgreichem Erwerb der exklusiven Lease geschlossen. ### Regelmäßiger Sync mit systemd Die mitgelieferte Unit ist ein `oneshot`-Dienst mit einem 15-Minuten-Timer. Passe gegebenenfalls `/opt/netbox-plugin-store` und `www-data` an: ```bash sudo cp deploy/netbox-plugin-store-sync.service /etc/systemd/system/ sudo cp deploy/netbox-plugin-store-sync.timer /etc/systemd/system/ sudo systemctl daemon-reload sudo systemctl enable --now netbox-plugin-store-sync.timer systemctl list-timers netbox-plugin-store-sync.timer journalctl -u netbox-plugin-store-sync.service ``` Alternativ kann ein dauerhafter Prozess verwendet werden: ```bash sudo -u www-data php bin/console sync --watch --interval=900 ``` Oder per Cron (keine überlappenden Läufe; der Store blockiert parallele Syncs zusätzlich): ```cron */15 * * * * cd /opt/netbox-plugin-store/store && /usr/bin/php bin/console sync >>/var/log/netbox-plugin-store-sync.log 2>&1 ``` ## 2. MariaDB statt JSON (optional) Der MariaDB-Adapter nutzt dieselbe Repository-/State-Schnittstelle. Er legt beim ersten Start automatisch eine einzelne, per InnoDB-Transaktion und `SELECT … FOR UPDATE` geschützte State-Zeile an. Das äquivalente Schema liegt unter `config/schema.sql`. ```dotenv STORE_DB_DRIVER=mariadb STORE_MARIADB_DSN=mysql:host=127.0.0.1;port=3306;dbname=netbox_store;charset=utf8mb4 STORE_MARIADB_USER=netbox_store STORE_MARIADB_PASSWORD= ``` Der Datenbankbenutzer benötigt `CREATE`, `SELECT`, `INSERT` und `UPDATE` auf der Store-Datenbank. Sichere die Datenbank wie andere Produktionsdaten regelmäßig. ## 3. Quellen und automatische Erkennung Neue Quellen werden in `/admin` zunächst als `pending` angelegt und müssen per POST/CSRF freigegeben werden. Verfügbare Provider: - `forgejo` für Forgejo und Gitea; - `github` für GitHub-User oder -Organisationen. Tokens werden nur aus dem in der Source hinterlegten Namen einer Umgebungsvariable gelesen, beispielsweise `GITEA_TOKEN` oder `GITHUB_TOKEN`; Tokenwerte werden nie im Datastore gespeichert. Ein Authorization-Header wird nur gesendet, wenn die Ziel-Origin exakt der `apiUrl`-Origin entspricht, und nach jedem Redirect neu bewertet. Private GitHub-Repositories dürfen mit einem entsprechend berechtigten Token für Metadaten und README eingelesen werden. Ihre Release-Assets werden in v1 bewusst fail-closed übersprungen: Der Store betreibt keinen Artefakt-Cache, und der Host-Agent besitzt keine GitHub-Credentials für einen späteren Download. Der Sync zeigt dafür einen Moderationshinweis und zieht eventuell ältere installierbare Releases dieses Repositorys zurück. Installierbar sind von GitHub ausschließlich öffentliche `.whl`-Assets über deren `browser_download_url`; Provider-Tokens werden niemals an Asset-Hosts weitergereicht. Die zuverlässigste Discovery gelingt mit einem Manifest im Repository-Root: ```json { "name": "Mein NetBox Plugin", "summary": "Kurze Beschreibung", "package_name": "netbox-mein-plugin", "import_name": "netbox_mein_plugin", "version": "1.2.3", "min_netbox_version": "4.6.5", "max_netbox_version": "4.6.8" } ``` Alternativ werden PEP-621/Poetry/Setuptools-Metadaten, Entry Points, `setup.py` und `PluginConfig` ausgewertet. Dynamische Setuptools-Versionen über `tool.setuptools.dynamic.version.attr` werden aus dem am gleichen Commit gepinnten Modul gelesen. Bleiben Paket, Top-Level-Import oder Kompatibilitätsgrenzen unbekannt, blockiert die Admin-Seite die Freigabe mit einer klaren Meldung. Dort können die Felder korrigiert werden; diese Overrides bleiben bei späteren Syncs erhalten. ## 4. Ein Wheel veröffentlichen und freigeben Die aktuell eingelesenen MrBlake-Repositories besitzen überwiegend keine Forgejo-Release-Wheels. Sie können nach Plugin-Freigabe im Store erscheinen, sind aber nicht automatisch installierbar. Empfohlener Release-Ablauf im Plugin-Repository: ```bash python -m pip install --upgrade build python -m build ``` 1. Einen unveränderlichen Git-Tag setzen. 2. In Forgejo einen Release für diesen Tag erstellen. 3. Das erzeugte `dist/*.whl` als Release-Asset hochladen. 4. Den Store synchronisieren. 5. Im Admin zunächst fehlende Plugin-Metadaten korrigieren/freigeben und anschließend genau das neue Release-Artefakt freigeben. Beim Sync lädt der Store das Wheel tatsächlich, berechnet SHA-256 und `artifact_size` und speichert den Commit-SHA. Neue Releases erben niemals die Plugin-Freigabe. Ändern sich URL, Hash, Größe, Commit, Paket-/Importname oder Kompatibilitätsgrenzen, werden Plugin bzw. Release wieder `pending`. Entfernte Upstream-Releases werden als zurückgezogen markiert und verschwinden sofort aus dem öffentlichen Katalog. Ein Git-Tag oder Quellcode-Archiv allein erzeugt kein installierbares Release. Der Store erzeugt keine Ersatzartefakte; NetBox-Client und Host-Agent installieren ausschließlich separat veröffentlichte und freigegebene Wheels. ## 5. Moderationsablauf Unter `/admin` stehen ausschließlich POST-Aktionen mit CSRF-Schutz bereit: 1. Source prüfen und freigeben; 2. Source synchronisieren; 3. Plugin-Metadaten prüfen/korrigieren und Plugin freigeben; 4. Wheel-Hash, Größe, Version und Kompatibilität prüfen und Release separat freigeben. Der Store erzeugt beim Release-Approval einen kanonischen `approved_payload_sha256`. Die öffentliche API liefert ein Artefakt nur, wenn der aktuelle Payload weiterhin exakt zu diesem Approval-Hash passt. Das ist unabhängig von den sichtbaren Statusfeldern eine zusätzliche Defense-in-Depth-Prüfung. ## 6. API v1 Alle Routen tolerieren einen abschließenden Slash: ```text GET /api/v1/plugins/ GET /api/v1/plugins/:slug/ GET /api/v1/plugins/:slug/releases/:version/ ``` Die Liste ist paginiert (`page`, `page_size`) und unterstützt `q`, `source` und `netbox_version`. Sie enthält ausschließlich aktive, vollständig validierte und freigegebene Source-/Plugin-Ketten. Plugin-Detail (gekürzt auf ein Release): ```json { "api_version": "v1", "slug": "demo-plugin", "name": "Demo Plugin", "summary": "Kurzbeschreibung", "description": "Beschreibung", "repository_url": "https://git.example.com/team/demo", "latest_version": "1.2.3", "package_name": "netbox-demo", "import_name": "netbox_demo", "min_netbox_version": "4.6.5", "max_netbox_version": "4.6.8", "approved": true, "status": "approved", "releases": [ { "version": "1.2.3", "download_url": "https://git.example.com/assets/netbox_demo-1.2.3-py3-none-any.whl", "sha256": "<64-hex>", "artifact_size": 12345, "commit_sha": "", "min_netbox_version": "4.6.5", "max_netbox_version": "4.6.8", "published_at": "2026-08-20T10:00:00+00:00", "approved": true, "status": "approved", "immutable": true, "approved_payload_sha256": "<64-hex>" } ] } ``` Die Release-Detailroute liefert exakt das flache Release-Objekt aus `releases[]`, ohne interne Admin-ID. Ohne freigegebenes Wheel ist `releases` leer und `latest_version` `null`. Ein Plugin-Detail enthält höchstens 1.000 Releases. `commit_sha` ist entweder leer oder exakt 40 kleingeschriebene Hex-Zeichen. Vor der Freigabe validiert der Store außerdem den Wheel-Dateinamen und verlangt, dass dessen normalisierte Distribution und Version exakt zu `package_name` und `version` passen; damit scheitert ein Artefakt nicht erst später im Host-Agent. ## 7. Netzwerk- und SSRF-Schutz Ausgehende Ziele müssen credential-freies HTTPS verwenden und in `STORE_ALLOWED_SOURCE_HOSTS` stehen. Jeder DNS-A/AAAA-Wert wird geprüft; Loopback, private, Link-Local- und reservierte Adressen sind standardmäßig gesperrt. cURL wird an die geprüfte Adresse gepinnt, Redirects werden einzeln erneut aufgelöst/geprüft, und Provider-Tokens bleiben auf der exakten API-Origin. ```dotenv STORE_ALLOWED_SOURCE_HOSTS=git.mrblake.cc,github.com,api.github.com,*.github.com,*.githubusercontent.com STORE_ALLOW_PRIVATE_NETWORKS=false ``` Für ein bewusst internes Forgejo muss dessen Host explizit allowgelistet und `STORE_ALLOW_PRIVATE_NETWORKS=true` gesetzt werden. Diese Ausnahme erweitert den SSRF-Radius und sollte nur in einem kontrollierten Netz verwendet werden. `STORE_TRUST_PROXY=true` darf nur genutzt werden, wenn direkter Zugriff auf Apache per Firewall ausgeschlossen ist. Zusätzlich werden Forwarded-IPs nur von exakt gelisteten Peers akzeptiert: ```dotenv STORE_TRUST_PROXY=true STORE_TRUSTED_PROXY_IPS=10.20.0.10,2001:db8::10 ``` ## 8. Optionale Docker-Installation Das Image enthält Apache und PHP 8.4; ein Proxy ist nicht erforderlich. Der Scheduler läuft im Container als `www-data`, damit Webprozess und Watcher dieselben geschützten JSON-Dateien lesen können. ```bash cd store docker build -t netbox-plugin-store . docker volume create netbox-plugin-store-data docker run -d --name netbox-plugin-store \ -p 3000:80 \ --env-file .env \ -e STORE_PUBLIC_URL=http://localhost:3000 \ -e STORE_COOKIE_SECURE=false \ -e STORE_JSON_PATH=/var/www/html/data/store.json \ -e STORE_SCHEDULER_ENABLED=true \ -v netbox-plugin-store-data:/var/www/html/data \ netbox-plugin-store ``` Der Healthcheck ist `GET /healthz`. Für Produktion HTTPS konfigurieren und `STORE_PUBLIC_URL`/Cookie-Einstellung entsprechend setzen. ## 9. Update, Backup und Diagnose Native Aktualisierung: ```bash cd /opt/netbox-plugin-store git pull --ff-only cd store composer install --no-dev --no-interaction --prefer-dist --classmap-authoritative sudo systemctl reload apache2 sudo -u www-data php bin/console sync ``` Vor Updates den JSON-Datastore oder MariaDB sichern. Der JSON-Store darf nur kopiert werden, während kein Schreibvorgang läuft; am einfachsten Apache/Timer kurz stoppen oder ein konsistentes dateisystemseitiges Backup-Verfahren verwenden. Nützliche Prüfungen: ```bash php -l public/index.php composer validate --strict composer test curl -fsS http://127.0.0.1/healthz curl -fsS http://127.0.0.1/api/v1/plugins/ ``` Sync-Ergebnisse stehen im Admin-Dashboard und werden als JSON vom CLI ausgegeben. Einzelne Repository-Fehler führen zu einem `partial`-Lauf und archivieren den letzten bekannten Eintrag nicht; eine erfolgreich festgestellte Nicht-Kandidatur oder ein erfolgreich festgestellter Release-Rückzug wird dagegen fail-closed aus dem öffentlichen Katalog entfernt. ## Tests Der zero-dependency Runner nutzt PHPs Laufzeit direkt; die Anwendungsabhängigkeiten müssen per Composer installiert sein: ```bash composer install composer test ``` Abgedeckt sind unter anderem API-Feldvertrag und 1.000er-Grenze, Approval-Payload, Wheel-Distribution/-Version/-Dateiname, 40-stellige Commit-SHAs, strikte Versions-/Metadatenvalidierung, README-Sanitizing und Commit-Pinning, Origin-Token-Isolation, private GitHub-Assets, private-IP-SSRF, atomare JSON-Grenzen und exklusive Leases, aggregierte Sync-Budgets, Trusted-Proxy-Auswertung, sticky Admin-Overrides, Rehash bei ersetzten Assets, Withdrawal und Nicht-Kandidaten-Archivierung.