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 | Y | |
| Agent group management | Y | ||
| Database health | Y | ||
| Audit log access | Y | ||
| Fleet monitoring | Y |
Default limits:
| Tier | Max Sessions | Max Agents |
|---|---|---|
| Professional | 100 | 3 |
| Business | 750 | 15 |
| MSP | 3,000 | 40 |
Limits are configurable per key at creation time. No tier is unlimited; null (no limit) is an admin-only per-key override, never a tier default.
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://portal.netvulntool.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 the admin JWT from the portal's OWN Identity service. Netlify
# Identity is per site, so the token has to be minted by the same host that
# will later accept it. This endpoint is not a portal function, so it is not
# intercepted at the edge.
TOKEN=$(curl -s -X POST "https://portal.netvulntool.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. Everything under /api/ IS a portal function,
# so the token travels in X-Identity-Token. Sending it in Authorization gets
# the request terminated at the edge before the handler runs.
curl -s -X POST "https://portal.netvulntool.com/api/licenses" \
-H "X-Identity-Token: $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://portal.netvulntool.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.
Add or change the key on an existing install
To add a key to an install that has none, or to change the key on one that has, run scripts/set_license_key.sh. Do not re-run install.sh for this: it rewrites your configs and leaves scanning turned off.
Read the key silently, then run the script:
bash
read -rs NV_LICENSE_KEY && export NV_LICENSE_KEY
bash scripts/set_license_key.shLeading and trailing whitespace in NV_LICENSE_KEY is trimmed, and the script removes the variable from its own environment so no child process inherits it. If no key is set in the environment, the script prompts for it on a terminal without echoing it. The key is never accepted as a command-line argument (--key and --license-key exit 2) and never appears on any command line, in output or in a log.
The script validates the key first. A malformed nvt_ key is refused, and a legacy key is accepted only if it uses letters, digits, ., _ and - and is at most 256 characters. Then it changes exactly two things:
- the license file,
license.keyin the.netvulndirectory (mode600, directory mode700). On a current-user install the file is replaced atomically (written mode600from the start, then renamed into place), and a directory or symlink atlicense.keyis refused. On thenvtinstall the key is installed through sudo as ownernvt, mode600, inside the700directory. NV_API_KEYin every*.conffile in the configs directory. An existing uncommentedNV_API_KEY=line is replaced in place, keeping anyexportprefix and indentation. A config with no such line getsNV_API_KEY="..."appended. Dot-files such as.c2_overrides.confand symlinks are skipped. Each edited config ends at mode600(owned bynvton thenvtinstall). Every other line is left exactly as it was. A config the script cannot read is never overwritten, and a configs directory that is unreadable (or, on a current-user install, not writable) is refused before anything is written.
It picks the install from where the script lives. On the service-account install under /opt/netvuln-tool it writes as the nvt account through sudo, using /home/nvt/.netvuln/license.key and /opt/netvuln-tool/configs. On a current-user install it writes ~/.netvuln/license.key and the configs in ${NVD_BASE_DIR:-$HOME/netvuln-tool}/configs, the same directory the daemon reads. It refuses to run from a tree other than the one the daemon runs from, or through sudo from a tree owned by someone else.
Afterwards it reloads the daemon so the new key takes effect: through systemd when the unit is active, or through scripts/netvuln_daemon_ctl.sh reload on a current-user install whose daemon is running. Otherwise the key takes effect at the next start. Pass --no-reload to skip the reload, or --help for the usage text.
| Exit code | Meaning |
|---|---|
| 0 | Key written (and the daemon reloaded, unless --no-reload) |
| 1 | Runtime failure: a write, the reload, or an install guard. A guard refuses before anything is written; after a partial write the message names the files already updated |
| 2 | Usage error, missing key, or invalid key |
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 | business | 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://portal.netvulntool.com/api/licenses?id=lic_abc123" \
-H "X-Identity-Token: $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://portal.netvulntool.com/api/licenses?id=lic_abc123" \
-H "X-Identity-Token: $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.
