Skip to content

Proxbox Backend Setup

The NetBox plugin requires a separate proxbox-api FastAPI service. The plugin stores configuration in NetBox, but sync requests are sent to this backend.

Backend Role

The current plugin code expects a configured FastAPIEndpoint object and uses it for:

  • device sync
  • virtual machine sync
  • full update
  • VM backup sync

Option 1: Install The Backend With pip

mkdir -p /opt/proxbox-api
cd /opt/proxbox-api
python3 -m venv venv
source venv/bin/activate
pip install --upgrade proxbox-api

Start it manually:

/opt/proxbox-api/venv/bin/uvicorn proxbox_api.main:app --host 0.0.0.0 --port 8800 --app-dir /opt/proxbox-api

Option 2: Run The Backend In Docker

docker pull emersonfelipesp/proxbox-api:latest
docker run -d --name proxbox-api -p 8800:8000 emersonfelipesp/proxbox-api:latest

The image serves on container port 8000 (nginx), so keep 8800:8000 if you want the backend reachable as http://<host>:8800.

If you want NetBox to connect over HTTPS, use the TLS (*-nginx) image instead:

docker pull emersonfelipesp/proxbox-api:latest-nginx
docker run -d --name proxbox-api-tls \
  -p 8800:8000 \
  -e MKCERT_EXTRA_NAMES='proxbox.backend.local' \
  emersonfelipesp/proxbox-api:latest-nginx

When configuring the NetBox FastAPIEndpoint for an *-nginx (TLS-only) image, set:

Field Value Why
Use HTTPS ✓ enabled The image only listens on TLS; plain HTTP returns 400.
Verify SSL ✗ disabled (when using the bundled mkcert cert) The cert is self-signed; verification will fail unless the mkcert root CA is trusted on the NetBox host. Tick only if you have installed the mkcert CA into NetBox's trust store.
Port host port mapped to container 8000 Typically 8800.

Use HTTPS and Verify SSL are independent toggles since v0.0.15 (issue #352). Earlier releases coupled them, which made the *-nginx + self-signed-cert combo unreachable from the UI.

Custom certificates

If you supply your own CA-signed, Let's Encrypt, or corporate certificates, the *-nginx and *-granian images detect them automatically and skip mkcert generation. Mount the certificate directory read-only:

docker run -d --name proxbox-api-tls \
  -p 8800:8000 \
  -v /path/to/certs:/certs:ro \
  emersonfelipesp/proxbox-api:latest-nginx

The /certs directory must contain cert.pem and key.pem. If either file is absent, mkcert auto-generation runs as normal.

When using a publicly trusted or internally-trusted certificate (not self-signed), you can enable certificate verification on the FastAPIEndpoint:

Field Value
Use HTTPS ✓ enabled
Verify SSL ✓ enabled
Port host port mapped to container 8000

For the granian image, see the full certificate handling notes in the proxbox-api installation docs.

Option 3: Run It As A systemd Service

This repository includes sample service files:

  • contrib/proxbox.service
  • contrib/proxbox-https.service

Install one of them:

sudo cp -v /opt/netbox/netbox/netbox-proxbox/contrib/proxbox.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now proxbox
sudo systemctl status proxbox

The sample unit expects the backend virtual environment under /opt/proxbox-api/venv and starts:

/opt/proxbox-api/venv/bin/uvicorn proxbox_api.main:app --host 0.0.0.0 --port 8800 --app-dir /opt/proxbox-api

Adjust the service file if your backend lives somewhere else.

Option 4: Run The Backend From Source

Use the source workflow when you want the latest backend code or need to patch the backend itself:

cd /opt
git clone https://github.com/emersonfelipesp/proxbox-api.git
cd /opt/proxbox-api

python3 -m venv venv
source venv/bin/activate
pip install -e .

/opt/proxbox-api/venv/bin/uvicorn proxbox_api.main:app --host 0.0.0.0 --port 8800 --app-dir /opt/proxbox-api

TLS Notes

If you terminate TLS in front of uvicorn with nginx, keep the proxy streaming-friendly:

  • proxy_pass to the local uvicorn process on 127.0.0.1:8000
  • set proxy_http_version 1.1
  • forward Host, X-Real-IP, X-Forwarded-For, and X-Forwarded-Proto
  • disable buffering with proxy_buffering off
  • keep long read/send timeouts so SSE /stream responses are not cut off early

For a complete nginx example, see the backend repository README and the bundled nginx templates under docker/nginx/.

If you use the HTTPS sample unit and point it at NetBox-managed certificates, the backend process may need permission to read them:

sudo chmod +rx -R /etc/ssl/private/
sudo chmod +rx -R /etc/ssl/certs/

That is convenient, but you should review the security impact for your environment before using it.

Authentication

The NetBox plugin and proxbox-api backend use database-backed API key authentication:

Fail-closed automatic token setup

The backend token field is optional. Saving an enabled FastAPIEndpoint without a token first records a fail-closed pending state. Automatic discovery then uses the exact URL/IP, port, and TLS-verification policy saved through the NetBox UI as its allowlist. It performs credential-free / and /health checks, with redirects disabled, before either of these flows:

  1. Locally held key — authenticate the already encrypted key with one read-only GET /auth/keys request. This repairs legacy blank fingerprints, activation, and configured target changes without asking the operator to paste the same secret again.
  2. Uninitialized backend (needs_bootstrap=true) — generate a strong key, retain it encrypted in NetBox, and require one successful POST /auth/register-key (201).

An initialized backend never receives the bootstrap POST. If it rejects the locally held key, or NetBox has no recoverable key, the row remains pending and runtime traffic stays blocked. The backend never exposes existing raw keys.

When no FastAPIEndpoint row exists, startup discovery is bounded to an explicit PLUGINS_CONFIG["netbox_proxbox"]["backend_url"] value (with optional boolean backend_verify_ssl, default true) and same-site backend.proxbox.<domain> / legacy proxbox.backend.<domain> names derived from NetBox's configured public origin. There is no network or subnet scan. After an endpoint is saved in the UI, that exact persisted target becomes the complete discovery allowlist; another IP or domain is rejected unless the operator first saves it as the endpoint configuration.

Authentication rejection, conflict, throttling, timeout, TLS failure, or a connection error leaves the previous encrypted token unchanged and the target untrusted. A disabled endpoint never makes a connection and a new disabled row remains keyless. The identity, bootstrap-status, key-list, and registration checks do not follow redirects, so credentials cannot be forwarded to another origin.

After adoption, the plugin stores a credential-free SHA-256 target fingerprint covering the canonical primary HTTP authority, fallback IP, port, HTTP/TLS flags, and WebSocket authority flags. Runtime callers recompute it before using the stored key and fail closed on target drift. The WebSocket client also disables ambient proxies, refuses a server-selected redirect before sending the key, rechecks trust after its handshake and periodically while connected, and is cancelled when the endpoint changes.

The generated first key is encrypted before it becomes available to runtime callers and is never logged or rendered. If the backend becomes initialized by another actor during bootstrap, the conflict remains pending rather than being treated as proof of authentication.

See Endpoint Auto-Configuration for the complete state machine, operator outcomes, requirements-to-tests matrix, and branch-coverage gate.

Manual Token Management

The diagnostic command never prints token fragments. Disabled endpoints are always skipped, including under --fix. Without --fix the command is read-only and does not contact an unadopted legacy target. After the operator reviews the target, --fix authenticates the durably stored key and records its target fingerprint; it performs a one-time bootstrap only when the backend confirms that no key exists:

# Check token status
python manage.py proxbox_fix_tokens

# Fix unregistered tokens
python manage.py proxbox_fix_tokens --fix

Safe key rotation

Keep the current key active until the replacement has been adopted and verified:

# 1. Authenticate with the current key and create a replacement.
curl -X POST http://localhost:8800/auth/keys \
  -H "X-Proxbox-API-Key: current-key"

The response exposes raw_key exactly once. Copy it directly into the existing NetBox FastAPIEndpoint token field. Saving performs a protected read with the candidate before changing the encrypted database value. Verify another protected backend request from the plugin, then deactivate or delete the old key. If any step fails, keep the old key active and retry only after resolving the reported condition. Automatic generation is limited to first-key bootstrap; it is never used as a rotation mechanism.

The unauthenticated POST /auth/register-key route is only for a backend whose bootstrap status explicitly reports that it has no keys. It is never a rotation mechanism, and HTTP 409 is a failure rather than proof that a candidate works.

Key Management

After the first key is registered, manage keys via the authenticated API:

# List keys (requires auth)
curl http://localhost:8800/auth/keys \
  -H "X-Proxbox-API-Key: your-key"

# Create a new key
curl -X POST http://localhost:8800/auth/keys \
  -H "X-Proxbox-API-Key: your-key"

# Delete a key
curl -X DELETE http://localhost:8800/auth/keys/1 \
  -H "X-Proxbox-API-Key: your-key"

Credential Encryption Key (required before the first Proxmox endpoint)

Do this before creating the Proxmox endpoint. The backend refuses to store any Proxmox credential until it has an encryption key, and it says so only at the moment you try:

Credential encryption is not configured, so this secret cannot be stored.
Set PROXBOX_ENCRYPTION_KEY, configure the ProxboxPluginSettings 'encryption_key' field,
create a local key via POST /admin/encryption/key, or set PROXBOX_ALLOW_PLAINTEXT_CREDENTIALS=1

That message lists every option; it simply arrives after you believe setup is finished.

Generate a key

python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"

This is the same value generate_encryption_key() produces inside the backend: a canonical URL-safe base64 Fernet key. Credentials are encrypted with Fernet (AES-128-CBC with HMAC), and the encrypted fields are the NetBox endpoint token and token key plus the Proxmox endpoint password and token value.

Supply it to the backend

Pick one source. PROXBOX_ENCRYPTION_KEY is the recommended one because the key then lives with the backend that uses it.

docker run -d --name proxbox-api \
  -p 8800:8000 \
  -e PROXBOX_ENCRYPTION_KEY='<paste the generated key>' \
  emersonfelipesp/proxbox-api:latest
# /etc/systemd/system/proxbox-api.service
[Service]
Environment=PROXBOX_ENCRYPTION_KEY=<paste the generated key>

Then systemctl daemon-reload && systemctl restart proxbox-api.

Have the backend generate a key and persist it to its own key file. The response returns the key once — store it somewhere safe, because losing it strands every credential encrypted with it:

curl -X POST http://localhost:8800/admin/encryption/generate \
  -H "X-Proxbox-API-Key: your-key"

To persist a key you generated yourself instead, POST /admin/encryption/key takes it in the body:

curl -X POST http://localhost:8800/admin/encryption/key \
  -H "X-Proxbox-API-Key: your-key" \
  -H "Content-Type: application/json" \
  -d '{"key": "<paste the generated key>"}'

GET /admin/encryption/status reports whether a key is configured and which source it came from (env, plugin, or local).

In Docker, set PROXBOX_ENCRYPTION_KEY_FILE as well

The key file defaults to data/encryption.key next to the installed package — inside the container that is /app/data/encryption.key, which is not on the image's declared /data volume. Recreating the container therefore destroys the key while the database on /data survives, stranding every credential encrypted with it. Point the key at the volume:

-e PROXBOX_ENCRYPTION_KEY_FILE=/data/encryption.key

PROXBOX_ENCRYPTION_KEY has no such problem, which is one more reason to prefer it for containerised deployments.

The remaining option, PROXBOX_ALLOW_PLAINTEXT_CREDENTIALS=1, stores Proxmox passwords and tokens unencrypted in the backend's SQLite database. It exists for local experimentation; do not use it anywhere real.

This is not the same key as the plugin's

Proxbox has three separate key domains and they must never be cross-copied: the proxbox-api-at-rest key covered here, the plugin-at-rest Fernet key that protects encrypted fields in NetBox's own database, and the FastAPI endpoint API key that authenticates HTTP requests. See Plugin Settings — Three separate security domains for the full comparison.

What happens if the key changes

Fernet ciphertext can only be decrypted by the key that produced it, so changing the key without re-encrypting makes every stored credential unreadable. Both key domains are protected against that, and neither silently discards data:

Plugin-at-rest key (NetBox database). Rotation is supported and verified. Ordinary form saves, model saves, and API PATCH requests cannot replace the key while any registered ciphertext exists; direct QuerySet.update() / bulk_update() / upsert writes to the key are rejected outright. Use Verified plugin key rotation on the settings page: it locks the settings row and every registered ciphertext table, verifies every non-empty ciphertext against the current key, and only then re-encrypts everything and stores the new key — all in one transaction. A wrong current key, or a single corrupt row, aborts the whole thing without changing any ciphertext or any setting. If the old key is genuinely lost, a separate netbox_proxbox.reset_encrypted_secrets permission gates a destructive reset that clears the affected secrets so they can be re-entered; there is no way to recover the plaintext without the key.

proxbox-api-at-rest key (backend SQLite). Re-point PROXBOX_ENCRYPTION_KEY and re-encrypt the backend database before restarting. Verified plugin rotation additionally requires every enabled, adopted, operational backend to return a successful authenticated GET /admin/encryption/status attestation confirming that its active key decrypts every stored credential — so a backend left on the old key blocks the plugin-side rotation rather than being silently stranded.

Full detail: Plugin Settings — Verified rotation and lost-key reset.

Next Step In NetBox

After the backend is reachable, create these objects in the Proxbox UI:

  1. Proxmox API
  2. NetBox API
  3. ProxBox API (FastAPI)

Then return to Plugins > Proxbox and run Full Update.