Skip to content

Marketplace

The FreeSDN Marketplace is a curated directory of installable plugins served from a remote registry. It provides browsing, version history, ratings, reviews, and one-click install - all gated behind Ed25519 catalog signing and SHA-256 download verification.

This page covers how browsing and installing works, the security model behind catalog signing, how to sign your own catalog for a private or third-party registry, and what the review system does.


How the Marketplace fits into the plugin system

Section titled “How the Marketplace fits into the plugin system”

The Marketplace is one path to getting a plugin onto your instance. The other path is direct ZIP upload (POST /api/v1/plugins/install). Both end up calling the same PluginLoader.install_plugin pipeline - the Marketplace simply adds a catalog layer on top: it knows where to download the ZIP, what version history exists, and what SHA-256 hash to expect.

The API lives at /api/v1/marketplace/plugins. The Swagger reference is at /api/v1/docs (unavailable in production; accessible in non-production environments when ENABLE_DOCS=true).


These endpoints are read-only and require no authentication - they are effectively public. No login or session is required to browse the catalog:

MethodPathPurpose
GET/api/v1/marketplace/pluginsPaginated browse - filter by category, sort by downloads / rating / newest / name
GET/api/v1/marketplace/plugins/featuredUp to six featured plugins
GET/api/v1/marketplace/plugins/categoriesCategory list with counts
GET/api/v1/marketplace/plugins/{slug}Plugin detail by slug
GET/api/v1/marketplace/plugins/{slug}/versionsFull version history
GET/api/v1/marketplace/plugins/{slug}/reviewsReviews, paginated (up to 50 per page)

GET /api/v1/marketplace/plugins accepts:

ParameterValuesNotes
qstring (≤ 128 chars)Substring search on name and description
categorystring (≤ 64 chars)Filter by category slug
sortdownloads, rating, newest, nameDefault: downloads
pageinteger ≥ 1Pagination cursor
per_page1-100Items per page

Available categories: monitoring, security, automation, integration, analytics, device, reporting.

The remote registry feed can set: name, description, author, version, download URL, checksum_sha256, tags, screenshots, icon, banner, and minimum version requirements.

The registry feed cannot set - and these values are ignored from the feed:

  • is_verified (whether FreeSDN has verified the publisher)
  • is_featured
  • status (draft / published / suspended / deprecated)
  • download_count
  • Database IDs or timestamps

This prevents a compromised or third-party registry from self-declaring “Verified by FreeSDN” status on a plugin. Those fields are controlled exclusively by your local database.


Before you can browse, your instance needs catalog data. Sync pulls from the remote registry and upserts the catalog into your local database:

POST /api/v1/marketplace/plugins/sync

Requires: super_admin.

Sync enforces the following:

  1. HTTPS only; no redirects; DNS-resolved to a public IP (DNS-rebind-safe); streamed response capped at 5 MB.
  2. JSON parsed - 502 on malformed JSON.
  3. Catalog signature verified (see the section below).
  4. Only the allowlisted fields above are written to the database.

If the remote registry is unreachable, FreeSDN falls back to a first-party seed file at backend/app/data/marketplace_seed.json. This file is trusted by provenance (it ships with the application source), not by signature check.


This is the most security-critical part of the Marketplace.

The catalog is signed with an Ed25519 key pair. The publisher signs a canonicalized JSON representation of the catalog (keys sorted, compact, signature field removed). The signature is a detached hex string delivered alongside the catalog JSON.

During sync, FreeSDN recanonicalizes the fetched catalog and verifies the signature against the pinned public key before touching the database.

MARKETPLACE_PUBLISHER_PUBLIC_KEY set?MARKETPLACE_ALLOW_UNSIGNED set?Behavior
Yes-Signature required and verified against pinned key. Mismatch → 403.
NoNo (default)Sync refused - 403. This is the default.
NoYes (1, true, or yes)Unsigned catalog accepted with a loud warning logged.

Set the MARKETPLACE_PUBLISHER_PUBLIC_KEY environment variable to the Ed25519 public key in hex. For the official FreeSDN registry, retrieve this key from the release notes or the project’s public signing manifest, then pin it in your deployment’s environment:

# .env.pro / .env.max
MARKETPLACE_PUBLISHER_PUBLIC_KEY=<hex-encoded-ed25519-public-key>

You cannot pin a key at runtime - it must be set before the process starts, because the variable is read at sync time.


POST /api/v1/marketplace/plugins/{slug}/install

Requires: super_admin.

What happens when you trigger a marketplace install:

  1. The catalog entry for {slug} is looked up in your local database (sync first if it is missing).
  2. The download_url from the catalog is fetched over HTTPS with DNS pinning, no redirects, and a 50 MB streaming cap.
  3. The downloaded bytes are SHA-256 hashed and compared against checksum_sha256 from the catalog entry. Mismatch → install aborted.
  4. The verified archive is passed to PluginLoader.install_plugin - the same ZIP pipeline used for direct uploads: zip-bomb protection (200 MB uncompressed cap), zip-slip rejection, manifest parse, optional hash-pinned dependency install, class load, on_install hook.
  5. The plugin record is persisted and the on_install hook is called. Note: unlike direct ZIP upload, marketplace install does not automatically start the plugin for active organizations - operators must enable or restart it after install.
  6. An audit row is written (tag: ["plugin","supply-chain","marketplace"]).
  7. The catalog’s download_count is incremented.
  8. HTTP 201 on success.

Use this when you operate a private registry or want to publish plugins outside the official FreeSDN registry. The signing tool ships with the backend:

backend/scripts/sign_marketplace_catalog.py
Terminal window
# Requires: Python ≥ 3.11 with the cryptography package
python scripts/sign_marketplace_catalog.py keygen
# Outputs PRIVATE_KEY_HEX and PUBLIC_KEY_HEX to stdout.
# Copy PUBLIC_KEY_HEX into MARKETPLACE_PUBLISHER_PUBLIC_KEY.
# Store PRIVATE_KEY_HEX offline - never commit it.

Store the private key outside your repository and outside your deployment containers. The hex public key goes into MARKETPLACE_PUBLISHER_PUBLIC_KEY on every FreeSDN instance that should trust your catalog.

Your catalog file must be valid JSON matching the expected schema (an array of plugin objects). To sign:

Terminal window
python scripts/sign_marketplace_catalog.py sign \
--key <PRIVATE_KEY_HEX> \
--in plugins.json \
--out plugins-signed.json

The tool:

  1. Reads your catalog JSON.
  2. Removes any existing signature field.
  3. Produces a canonicalized representation (keys sorted, compact, no whitespace).
  4. Signs that canonical bytes with the Ed25519 private key.
  5. Writes a new JSON file identical to the input but with a signature field added at the top level (hex-encoded signature).
MARKETPLACE_REGISTRY_URL=https://your-registry.example.com/plugins.json
MARKETPLACE_PUBLISHER_PUBLIC_KEY=<your-public-key-hex>

Both variables are read at sync time.

Every time you add, remove, or update a plugin entry in your catalog file, you must re-sign it. Serving a stale or unsigned catalog causes sync to fail with 403 on any instance with key pinning enabled.


Any active user can submit one review per plugin. Super_admin authority is not required.

POST /api/v1/marketplace/plugins/{slug}/reviews

Request body:

FieldTypeConstraints
ratinginteger1-5, required
titlestring≤ 200 characters, optional
bodystring≤ 4000 characters, optional

Constraints:

  • One review per user per plugin. A second submission returns 409.
  • After each new review the plugin’s aggregate rating and rating_count are recomputed.
  • Reviews are public reads - no authentication is required to list them via GET /api/v1/marketplace/plugins/{slug}/reviews.

VariableDefaultPurpose
MARKETPLACE_REGISTRY_URLhttps://registry.freesdn.org/plugins.jsonRemote catalog URL for sync
MARKETPLACE_PUBLISHER_PUBLIC_KEY(empty)Hex Ed25519 public key to require for signature verification
MARKETPLACE_ALLOW_UNSIGNED(unset / false)Set 1, true, or yes to accept unsigned catalogs - logs a loud warning

Sync returns 403 - “Catalog signature verification failed”

You have MARKETPLACE_PUBLISHER_PUBLIC_KEY set and the fetched catalog’s signature does not match. Causes:

  • The catalog file was updated but not re-signed.
  • You are pointing at the wrong registry URL.
  • Your key was rotated but the instance env was not updated.

Check that MARKETPLACE_REGISTRY_URL resolves to the intended host, that your public key hex matches the signing private key, and that the catalog was signed after its last edit.

Sync returns 403 - “Unsigned catalogs not permitted”

You have no key pinned and MARKETPLACE_ALLOW_UNSIGNED is not set. Either pin a key or explicitly opt in to unsigned operation.

Sync returns 502

The remote URL returned malformed JSON (invalid UTF-8 or a JSON parse error). Check that MARKETPLACE_REGISTRY_URL points to a valid JSON catalog. The seed fallback does not apply in this case - the 502 is returned immediately.

Sync succeeds but shows only first-party plugins (seed data)

The remote registry was unreachable (network error, DNS failure, or timeout). FreeSDN fell back to the bundled first-party seed file at backend/app/data/marketplace_seed.json. Only first-party plugins are available until the registry is reachable again. If no seed file is present, the response is 503 instead.

Install returns a checksum mismatch error

The downloaded archive’s SHA-256 does not match the catalog entry. This is either a corrupted download or a catalog / archive out of sync. Do not retry without investigating - a mismatch is a supply-chain signal.