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.pyundPluginConfig; - 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_rewriteundmod_headers; - Composer 2;
- PHP-Erweiterungen:
curl,dom,iconv,intl,json,mbstring,pdound für MariaDB zusätzlichpdo_mysql; für einen graceful beendbaren dauerhaften--watch-Prozess wirdpcntlempfohlen; - Schreibzugriff des Apache-/Sync-Benutzers auf
store/data.
Beispiel für Debian/Ubuntu (Paketnamen können je Distribution/PHP-Repository abweichen):
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
sudo mkdir -p /opt/netbox-plugin-store
sudo chown "$USER":"$USER" /opt/netbox-plugin-store
git clone <REPOSITORY-URL> /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:
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:
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:
STORE_ADMIN_USERNAME=admin
STORE_ADMIN_PASSWORD_HASH=$argon2id$...
STORE_SESSION_SECRET=<mindestens-32-zeichen>
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:
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 an:
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
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:
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:
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:
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):
*/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.
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=<starkes-passwort>
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:
forgejofür Forgejo und Gitea;githubfü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:
{
"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:
python -m pip install --upgrade build
python -m build
- Einen unveränderlichen Git-Tag setzen.
- In Forgejo einen Release für diesen Tag erstellen.
- Das erzeugte
dist/*.whlals Release-Asset hochladen. - Den Store synchronisieren.
- 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:
- Source prüfen und freigeben;
- Source synchronisieren;
- Plugin-Metadaten prüfen/korrigieren und Plugin freigeben;
- 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:
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):
{
"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": "<git-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.
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:
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.
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:
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:
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:
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.