Skip to content

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

FeatureProfessionalBusinessMSP
Upload sessionsYYY
Download reportsYYY
View sessionsYYY
Update statusYYY
Benchmark comparisonYY
Remediation trackingYY
Webhook subscriptionsYY
Compliance mappingYY
C2 / Agent commandsY
Agent group managementY
Database healthY
Audit log accessY
Fleet monitoringY

Default limits:

TierMax SessionsMax Agents
Professional505
Business50025
MSPUnlimitedUnlimited

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

  1. Log in to the admin dashboard at https://bullium.com/admin/
  2. Navigate to the Licenses tab
  3. Click Create License Key
  4. Fill in: tier, client ID, name, optional description, optional limits and expiry
  5. Copy the plaintext key shown (it will not be displayed again)
  6. 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:

  1. Replace the variable (recommended):

    bash
    # Old
    CONSULTATION_API_KEY="old-shared-key"
    # New
    NV_API_KEY="nvt_pro_<hex>"
  2. Keep both (NV_API_KEY takes precedence when both are set):

    bash
    CONSULTATION_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):

  1. NV_LICENSE_KEY environment variable (never shadowed by a config file; env-first)
  2. NV_API_KEY config value, then environment variable
  3. CONSULTATION_API_KEY config value, then environment variable (legacy alias)
  4. ~/.netvuln/license.key file

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

FeatureMinimum tierGate
Core scanning, reporting, uploadnone (community)never gated
Compliance mappingbusinesscompliance_mapper.sh
Benchmark comparisonbusinessbenchmark_compare.sh
Remediation updatebusinessreserved
Webhooksbusinessreserved
C2 / agent commandsmsplib/command_handler.sh
Unknown / future featuresmspfail 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: tier legacy, 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 tier legacy. 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, mode 600. 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:true answer restores the tier.
  • First run offline (no cache yet for the key) keeps the tier derived from the key format.
  • Hosts without jq skip online validation and rely on the format gate.

/api/license Troubleshooting

SymptomCauseResolution
License invalid (reason: revoked)Key was revokedRequest a new key from your administrator
License invalid (reason: expired)Past expires_atRequest a new key or an extension
License invalid (reason: inactive)Key deactivatedAsk your administrator to reactivate
License invalid (reason: not_found)Key unknown to the portalVerify the key was copied in full
using cached license (offline)Portal unreachable, within graceRestore connectivity before the grace window ends
beyond the grace windowOffline for more than TTL + graceReconnect; the next successful check restores the tier
HTTP 401 from /api/licenseMissing x-api-key headerCheck the config plumbing; the key did not reach the request

Key Lifecycle

StateDescriptionCan Upload?
ActiveNormal operating stateYes
InactiveAdministratively disabled (is_active: false)No
ExpiredPast expires_at dateNo
RevokedPermanently revoked by adminNo
Limit reachedSession or agent count at maximumNo

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 MessageCauseResolution
Invalid API keyKey not found in database or hash mismatchVerify the key was copied correctly; check for trailing whitespace
License key is inactiveKey was deactivated by adminContact your administrator to reactivate
License key has been revokedKey was permanently revokedRequest a new key from your administrator
License key has expiredKey's expires_at date has passedRequest a new key or ask admin to extend
Session limit reached (N)Client has N sessions, at the key's maximumArchive old sessions or request a limit increase
Agent limit reached (N)Client has N registered agents, at maximumDecommission unused agents or request a limit increase
Insufficient permissions for XKey's tier does not include the requested featureUpgrade 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.conf
  • API Contract: the GET /api/license response schema, tier permission matrix, and license validation deployment order.
  • Secrets Management: where license keys live, rotation procedures, and the two-key rule.

Apache-2.0 licensed (appliance subtree proprietary)