Appearance
License Key Provisioning
License keys replace the legacy shared API key with per-client, per-tier authentication for the NetVuln Tool collection portal.
What Changed
Previously, all NetVuln Tool deployments shared a single CONSULTATION_API_KEY (the NV_UPLOAD_API_KEY environment variable on the server). License keys provide:
- Per-client isolation: each client gets their own key with a
client_id - Tier-based permissions: feature access scales with the client's tier
- Usage limits: configurable session and agent caps per key
- Lifecycle management: keys can be expired, revoked, or deactivated
- Audit trail: every key usage is timestamped
Tier Comparison
| Feature | Professional | Business | MSP |
|---|---|---|---|
| Upload sessions | Y | Y | Y |
| Download reports | Y | Y | Y |
| View sessions | Y | Y | Y |
| Update status | Y | Y | Y |
| Benchmark comparison | Y | Y | |
| Remediation tracking | Y | Y | |
| Webhook subscriptions | Y | Y | |
| Compliance mapping | Y | Y | |
| C2 / Agent commands | Y | ||
| Agent group management | Y | ||
| Database health | Y | ||
| Audit log access | Y | ||
| Fleet monitoring | Y |
Default limits:
| Tier | Max Sessions | Max Agents |
|---|---|---|
| Professional | 50 | 5 |
| Business | 500 | 25 |
| MSP | Unlimited | Unlimited |
Limits are configurable per key at creation time.
Key Format
nvt_{tier}_{64 hex characters}The {tier} segment is one of pro, biz, or msp:
nvt_pro_<hex>(Professional)nvt_biz_<hex>(Business)nvt_msp_<hex>(MSP)
The key prefix (first 14 characters) is used for fast database lookup. The full key is SHA-256 hashed before storage; the plaintext is shown once at creation and cannot be recovered.
Admin Provisioning
Via Admin Dashboard
- Log in to the admin dashboard at
https://bullium.com/admin/ - Navigate to the Licenses tab
- Click Create License Key
- Fill in: tier, client ID, name, optional description, optional limits and expiry
- Copy the plaintext key shown (it will not be displayed again)
- Provide the key to the client securely
Via API
bash
# Acquire admin JWT (fill in your admin email and password)
TOKEN=$(curl -s -X POST "https://bullium.com/.netlify/identity/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=password" \
--data-urlencode "username=$ADMIN_EMAIL" \
--data-urlencode "password=$ADMIN_PW" \
| jq -r '.access_token')
# Create a professional-tier key
curl -s -X POST "https://bullium.com/api/licenses" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"tier": "professional",
"client_id": "acme-corp",
"name": "ACME Corp Production",
"max_sessions": 100
}' | jq '.license.plaintext_key'Client Setup
New Installation
In your report_config.conf:
bash
CONSULTATION_API_URL="https://bullium.com/api/upload"
NV_API_KEY="nvt_pro_<hex>"You can also place the key in the file-based resolution channel, ~/.netvuln/license.key. An installer or manual setup writes this file (keep it mode 600); it is the lowest-precedence source and is handy when you want the key available to every config on a host without repeating it.
Migration from Legacy Key
If you are currently using CONSULTATION_API_KEY, you have two options:
Replace the variable (recommended):
bash# Old CONSULTATION_API_KEY="old-shared-key" # New NV_API_KEY="nvt_pro_<hex>"Keep both (
NV_API_KEYtakes precedence when both are set):bashCONSULTATION_API_KEY="old-shared-key" # fallback NV_API_KEY="nvt_pro_<hex>" # used when set
The legacy CONSULTATION_API_KEY variable is still supported for backward compatibility.
Client-Side Validation (v4.1.0)
Canonical Key and Resolution Precedence
NV_API_KEY is the canonical license key. The scanner checks these names in order (the same order in lib/license.sh and netvuln/license.py):
NV_LICENSE_KEYenvironment variable (never shadowed by a config file; env-first)NV_API_KEYconfig value, then environment variableCONSULTATION_API_KEYconfig value, then environment variable (legacy alias)~/.netvuln/license.keyfile
For NV_API_KEY and CONSULTATION_API_KEY, a config-file value wins over the environment: sourcing the config overwrites those variables in bash, and the Python resolver matches that behavior.
The resolved key is parsed against nvt_{pro|biz|msp}_{64 hex} and the result is exported as NV_LICENSED (true/false), NV_LICENSE_TIER (professional/business/msp/legacy, empty for community), and NV_LICENSE_SOURCE (which channel the key came from). The plaintext key is never exported or logged.
Tier to Feature Map
| Feature | Minimum tier | Gate |
|---|---|---|
| Core scanning, reporting, upload | none (community) | never gated |
| Compliance mapping | business | compliance_mapper.sh |
| Benchmark comparison | business | benchmark_compare.sh |
| Remediation update | business | reserved |
| Webhooks | business | reserved |
| C2 / agent commands | msp | lib/command_handler.sh |
| Unknown / future features | msp | fail conservative |
Gates are soft: a feature below your tier prints a warning and is skipped; scans never fail because of licensing. The portal still enforces tiers server-side with HTTP 403.
Legacy and Malformed Keys
- A non-empty key that is not
nvt_-prefixed is treated as a legacy key: tierlegacy, all features enabled, one-time deprecation warning. - An
nvt_-prefixed key that fails the format check (for example, a truncated paste) warns loudly and also resolves to tierlegacy. The client never locks itself out; the portal remains authoritative.
Online Validation Cache and Grace Window
When a portal URL and key are configured, the scanner validates the key against GET /api/license once per pipeline run and once per daemon heartbeat cycle (lib/license_validate.sh, netvuln/license.py).
- Cache file:
~/.netvuln/license_cache.json, mode600. It stores a SHA-256 fingerprint prefix of the key, never the plaintext. - TTL: 24 hours (
NV_LICENSE_TTL_SECONDS, default 86400). A fresh cache skips the network entirely. - Grace: a further 72 hours (
NV_LICENSE_GRACE_SECONDS, default 259200). If the portal is unreachable, the cached tier keeps working through the grace window, then the scanner downgrades to community features. - A revoked, expired, or inactive response downgrades immediately with a warning naming the reason. The cached negative result stays authoritative even when the portal later becomes unreachable; only a fresh
valid:trueanswer restores the tier. - First run offline (no cache yet for the key) keeps the tier derived from the key format.
- Hosts without
jqskip online validation and rely on the format gate.
/api/license Troubleshooting
| Symptom | Cause | Resolution |
|---|---|---|
License invalid (reason: revoked) | Key was revoked | Request a new key from your administrator |
License invalid (reason: expired) | Past expires_at | Request a new key or an extension |
License invalid (reason: inactive) | Key deactivated | Ask your administrator to reactivate |
License invalid (reason: not_found) | Key unknown to the portal | Verify the key was copied in full |
using cached license (offline) | Portal unreachable, within grace | Restore connectivity before the grace window ends |
beyond the grace window | Offline for more than TTL + grace | Reconnect; the next successful check restores the tier |
HTTP 401 from /api/license | Missing x-api-key header | Check the config plumbing; the key did not reach the request |
Key Lifecycle
| State | Description | Can Upload? |
|---|---|---|
| Active | Normal operating state | Yes |
| Inactive | Administratively disabled (is_active: false) | No |
| Expired | Past expires_at date | No |
| Revoked | Permanently revoked by admin | No |
| Limit reached | Session or agent count at maximum | No |
Revoking a Key
Revocation is permanent. Once revoked, the key cannot be reactivated, and a new key must be created.
bash
curl -s -X PATCH "https://bullium.com/api/licenses?id=lic_abc123" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"action": "revoke"}'Deleting a Key
Keys must be revoked before deletion (two-step safety):
bash
curl -s -X DELETE "https://bullium.com/api/licenses?id=lic_abc123" \
-H "Authorization: Bearer $TOKEN"Troubleshooting
| Error Message | Cause | Resolution |
|---|---|---|
Invalid API key | Key not found in database or hash mismatch | Verify the key was copied correctly; check for trailing whitespace |
License key is inactive | Key was deactivated by admin | Contact your administrator to reactivate |
License key has been revoked | Key was permanently revoked | Request a new key from your administrator |
License key has expired | Key's expires_at date has passed | Request a new key or ask admin to extend |
Session limit reached (N) | Client has N sessions, at the key's maximum | Archive old sessions or request a limit increase |
Agent limit reached (N) | Client has N registered agents, at maximum | Decommission unused agents or request a limit increase |
Insufficient permissions for X | Key's tier does not include the requested feature | Upgrade to a higher tier |
E2E Testing
A license lifecycle test suite lives at tests/e2e/test_license_lifecycle.sh. It validates all key states and tier boundaries against a live portal deployment, configured via tests/e2e/e2e.conf (copy the tracked tests/e2e/e2e.conf.example template):
bash
cp tests/e2e/e2e.conf.example tests/e2e/e2e.conf
# Edit e2e.conf with your dev portal admin credentials
./tests/e2e/test_license_lifecycle.sh -c tests/e2e/e2e.confRelated
- API Contract: the
GET /api/licenseresponse schema, tier permission matrix, and license validation deployment order. - Secrets Management: where license keys live, rotation procedures, and the two-key rule.
