Skip to content

Secrets & Environment Management

Cross-project credential inventory, environment setup, and rotation practices for the NetVuln Tool and bullium-site ecosystem.

Audience: developers and operators working on either repository. Canonical source: netvuln-tool/docs/SECRETS_MANAGEMENT.md (this page mirrors it).

WARNING

This page never contains real secret values. Every credential below is described by name, location, and risk only. Placeholders (nvt_pro_<hex>, napi_xxx, glpat-xxx) stand in for real values.

Secret Inventory

Every credential used across both projects, where it lives, and what it controls.

bullium-site (Portal)

SecretEnv VarWhere SetScopeRisk if Leaked
Neon PostgreSQL URLNEON_DATABASE_URLNetlify dashboard (per-branch context)DB read/write for all session metadataCRITICAL: full DB access
Neon PostgreSQL URL (fallback)NETLIFY_DATABASE_URLNetlify dashboardSame as above (alternative name)CRITICAL
Legacy upload API keyNV_UPLOAD_API_KEYNetlify dashboardAuthenticates CLI uploads (legacy path)HIGH: upload access
Plausible API keyPLAUSIBLE_API_KEYLocal .env onlyRead-only analytics queriesLOW: analytics data only
Neon management API keyNEON_DB_KEYLocal .env onlyNeon dashboard API (branch mgmt)MEDIUM: DB admin ops
GitLab personal access tokenGITLAB_TOKENLocal .env onlyGitLab API for CI/MR automationHIGH: repo write access
Netlify auth tokenNETLIFY_AUTH_TOKENLocal .env only (migration)Netlify API for blob migrationMEDIUM: one-time use

Netlify Identity (managed by Netlify, not stored locally):

  • JWT signing keys: managed by Netlify, no local storage
  • User credentials: stored in the Netlify Identity service
  • app_metadata (roles, client_ids): set via the Netlify admin UI

netvuln-tool (Scanner)

SecretConfig VarWhere SetScopeRisk if Leaked
Portal API URLCONSULTATION_API_URLreport_config.confUpload endpointLOW: URL only, not a secret
License key (current)NV_API_KEYreport_config.conf or env varUpload auth, tier-gatedHIGH: upload + tier access
Legacy API keyCONSULTATION_API_KEYreport_config.confSame as NV_API_KEY (deprecated alias)HIGH
Enterprise licenseNV_LICENSE_KEYEnv var or ~/.netvuln/license.keyFeature gatingLOW: no remote access
Slack webhook URLALERT_SLACK_WEBHOOKreport_config.confPost messages to a Slack channelMEDIUM: spam risk
Alert webhook URLALERT_WEBHOOK_URLreport_config.confPOST to an external webhookMEDIUM
Alert emailALERT_EMAILreport_config.confSend email alertsLOW
License cachen/a~/.netvuln/license_cache.json (mode 600)Online validation cacheNONE: SHA-256 fingerprint prefix only, never the plaintext key; not a secret but kept 600 for hygiene

See License Provisioning for how NV_API_KEY, CONSULTATION_API_KEY, NV_LICENSE_KEY, and ~/.netvuln/license.key resolve in precedence order.

CI/CD Secrets

SecretPlatformWhere SetUsed By
STAGING_API_URLGitea ActionsRepository secretsE2E tests (e2e.yml)
STAGING_API_KEYGitea ActionsRepository secretsE2E tests (e2e.yml)

Environment Setup

bullium-site: Local Development

  1. Copy the template:

    bash
    cp .env.example .env
  2. Populate required values:

    bash
    # Required for function development
    NEON_DATABASE_URL=postgresql://user:pw@ep-xxx.us-east-2.aws.neon.tech/neondb?sslmode=require
    
    # Required for CLI upload testing
    NV_UPLOAD_API_KEY=your-upload-key
    
    # Optional: analytics (marketing agent only)
    PLAUSIBLE_API_KEY=your-plausible-key
    
    # Optional: Neon admin API (branch management only)
    NEON_DB_KEY=napi_xxx
    
    # Optional: GitLab API (MR automation only)
    GITLAB_TOKEN=glpat-xxx
  3. Run the local dev server:

    bash
    npx netlify dev --port 8888

    Netlify Dev automatically loads .env and injects the vars into functions.

bullium-site: Netlify Dashboard Configuration

Environment variables are set per-context in the Netlify dashboard:

ContextNEON_DATABASE_URLNV_UPLOAD_API_KEYNotes
Production (main)Points to Neon main branchProduction upload keyAuto-deploys on merge
Dev branch deployPoints to Neon dev branchSame or test keySchema validation first
Deploy previewPoints to Neon dev branchTest keyPR preview deploys

DANGER

The dev and production NEON_DATABASE_URL values must point to different Neon database branches. This is what makes the schema validation workflow safe: drizzle-kit push on the dev deploy hits the dev DB copy, not production.

netvuln-tool: Scanner Configuration

  1. Copy the config template:

    bash
    cp templates/report_config.conf.example ~/netvuln-tool/configs/my-client.conf
  2. Populate credentials:

    bash
    # Portal connection
    CONSULTATION_API_URL="https://bullium.com/api/upload"
    NV_API_KEY="nvt_pro_<hex>"
    
    # Optional: daemon mode
    DAEMON_ENABLED="true"
    DAEMON_HEARTBEAT_INTERVAL="300"
    
    # Optional: alerting
    ALERT_ENABLED="true"
    ALERT_EMAIL="admin@example.com"
  3. Verify connectivity:

    bash
    bash scripts/e2e_smoke_test.sh -c ~/netvuln-tool/configs/my-client.conf

netvuln-tool: Multi-Client Setup

For MSP operators managing multiple clients, each client gets its own config:

~/netvuln-tool/configs/
├── acme-corp.conf      # CLIENT_ID="acme-corp", unique NV_API_KEY
├── widgets-inc.conf    # CLIENT_ID="widgets-inc", unique NV_API_KEY
└── city-of-xxx.conf    # CLIENT_ID="city-of-xxx", unique NV_API_KEY

The daemon loads all configs and runs scans per-schedule per-config. Each NV_API_KEY is a separate license key tied to a client_id in the portal.

Dev vs Production Credential Separation

The Two-Database Rule

Never use production database credentials for local development or testing.

EnvironmentNeon BranchHow to identify
Local dev (netlify dev)devURL contains the ep-xxx dev endpoint
Netlify dev deploydevSet in the Netlify dashboard context
Deploy previewdevSet in the Netlify dashboard context
ProductionmainSet in the Netlify dashboard context

Neon branching gives you a copy-on-write fork. Schema changes on dev do not touch production data. If you break the dev DB, reset it from the Neon dashboard.

The Two-Key Rule

Use separate API keys for testing vs production:

PurposeKey SourceNotes
Local testingGenerate a test license key via the admin panelLower tier (professional) is fine
CI/CD E2E testsSTAGING_API_KEY in Gitea Actions secretsPoints at the dev deploy
Client productionPer-client license key via the admin panelTier matches their subscription
Legacy fallbackNV_UPLOAD_API_KEY env var in NetlifySingle key, no per-client scoping

Key Rotation Schedule

When to Rotate

TriggerActionUrgency
Suspected compromiseRotate immediately, audit access logsIMMEDIATE
Team member leavesRotate all keys they had access toWithin 24 hours
Key found in logs/settingsRotate the exposed keyWithin 24 hours
Routine maintenanceRotate highest-risk keysQuarterly
Netlify Identity user removedNo key rotation needed (JWT-based)N/A

Rotation Procedures

License Keys (nvt_* keys)

License keys are the primary auth mechanism for scanner-to-portal communication.

To rotate a license key:

  1. Generate a new key in the admin panel (Licenses tab, Create)
    • Same tier and client_id as the old key
  2. Update scanner configs that use the old key
    • Edit NV_API_KEY in each affected report_config.conf
    • If the daemon is running: netvuln_daemon_ctl.sh reload (picks up the new config)
  3. Revoke the old key in the admin panel (Licenses tab, Revoke)
    • Revoked keys return 403 immediately
    • Grace period: do steps 1 and 2 for all scanners before revoking
  4. Verify with a test upload or heartbeat

Risk of skipping: the old key continues to work until revoked. There is no automatic expiry unless the key has an expiry date set at creation.

Neon Database URL

To rotate the database password:

  1. Go to the Neon dashboard, then Project, then Connection details
  2. Reset the password for the relevant branch (dev or main)
  3. Copy the new connection string
  4. Update it in the Netlify dashboard (Settings, Environment Variables, edit the context-specific NEON_DATABASE_URL)
  5. Update local .env if you changed the branch you use locally
  6. Trigger a redeploy: git commit --allow-empty -m "chore: rotate db credentials" && git push

Risk of skipping: Neon passwords do not expire automatically. Rotate if the connection string has been exposed (for example in logs, screenshots, or shared .env files).

Plausible API Key

  1. Go to the Plausible dashboard, then Settings, then API Keys
  2. Revoke the old key, create a new one
  3. Update the local .env file: PLAUSIBLE_API_KEY=new-key-here
  4. Update any Claude Code scheduled tasks that use it (marketing pulse)

Risk of skipping: read-only analytics access. Low urgency unless the key is published publicly.

GitLab Personal Access Token

  1. Go to GitLab, then Preferences, then Access Tokens
  2. Revoke the old token, create a new one with the same scopes
  3. Update the local .env file: GITLAB_TOKEN=glpat-new-token

Risk of skipping: depends on the token scopes. If write_repository is enabled, an attacker could push code to your repos.

Gitea API Token

  1. Go to Gitea, then Settings, then Applications, then Access Tokens
  2. Delete the old token, create a new one
  3. Update any scripts or settings files referencing it
  4. Clean the old token from .claude/settings.local.json command history

Secret Hygiene Checklist

Things That Should NEVER Contain Real Secrets

LocationWhy It's RiskyWhat to Do
Git historyPermanent, even after deletionUse .gitignore, never commit .env
.claude/settings.local.jsonAccumulates approved commands with tokensPeriodically audit and clean
Shell history (~/.bash_history)Plaintext command logUse env vars, not inline tokens
CI/CD logsMay be publicly readableNever echo secrets, use --silent in curl
ScreenshotsEasy to share accidentallyRedact before sharing
Slack/emailPersisted in third-party systemsUse secure vaults, never paste keys

Pre-Commit Checks

Both repos use .gitignore to block secret files. Additionally:

bullium-site blocks:

  • .env, .env.* (except .env.example)
  • *.key, *.pem, creds.conf, secrets.json

netvuln-tool blocks:

  • .env, .env.*
  • *.key, *.pem, creds.conf, secrets.json
  • tests/e2e/e2e.conf
  • All *.conf files (except templates)
  • **/report_config.conf (explicit)

Periodic Audit

Quarterly, run these checks:

bash
# Check for accidental secret commits (bullium-site)
git log --all --diff-filter=A -- '*.env' '*.key' '*.pem' 'creds.conf'

# Check for leaked patterns in tracked files
grep -rn 'nvt_[a-z]*_[a-f0-9]' --include='*.js' --include='*.mjs' --include='*.html'
grep -rn 'glpat-' --include='*.js' --include='*.json' --include='*.md'
grep -rn 'napi_' --include='*.js' --include='*.json'

# Check .claude settings for accumulated secrets
grep -n 'Bearer\|x-api-key\|glpat-\|napi_\|nvt_' .claude/settings.local.json

Architecture Decision: Why Not a Vault?

For a solo-founder operation with AI-assisted development, a full secrets vault (HashiCorp Vault, AWS Secrets Manager, and the like) adds operational complexity that outweighs the benefit. The current approach is appropriate because:

  1. Netlify dashboard handles production secrets (encrypted at rest, per-context)
  2. Neon branching separates dev from prod at the database level
  3. License keys provide per-client isolation with tier-based permissions
  4. .gitignore plus netlify.toml forced 404s provide two-layer exposure prevention
  5. Config file sourcing (not env vars in shell history) reduces local exposure

When to reconsider: if you onboard employees or contractors who need partial access, a vault becomes worthwhile. The current approach is a pragmatic starting point; the goal is to migrate to a proper vault as the operation matures.

Vault Migration Roadmap

The current Netlify dashboard plus .env approach is Phase 1. The plan is to adopt a secrets vault for centralized management, audit logging, and automatic rotation.

Phase 1 (current): Netlify dashboard, local .env files, and manual rotation. Documented above. Sufficient for a solo-founder operation.

Phase 2 (planned): evaluate vault options and migrate the highest-risk secrets.

Candidates to evaluate:

  • Doppler: SaaS, no-cost tier, integrates with Netlify and CI/CD. Good for small teams. Syncs secrets to Netlify env vars automatically.
  • 1Password Secrets Automation: if already using 1Password for personal credentials. The CLI (op) integrates with shell scripts and CI.
  • HashiCorp Vault: self-hosted or HCP (cloud). Most powerful, most complex. Best if you already run infrastructure (for example on Proxmox).
  • Infisical: open-source, self-hostable, Netlify integration. Aligns with the open-source ethos of the project.

Migration order (highest risk first):

  1. NEON_DATABASE_URL: full DB access, most critical
  2. NV_UPLOAD_API_KEY / license keys: upload authentication
  3. GITLAB_TOKEN: repo write access
  4. PLAUSIBLE_API_KEY, NEON_DB_KEY: lower risk, migrate last

Phase 3 (future): automatic rotation via vault policies. The vault generates short-lived credentials and eliminates long-lived keys entirely. This requires vault integration in both the Netlify build pipeline and the scanner config loading.

Learning resources:

  • API Contract: full endpoint specs, auth schemes, and shared constants.
  • License Provisioning: license key lifecycle, tiers, and precedence.
  • bullium-site CLAUDE.md: security section (two-layer protection, CSP, HSTS).
  • bullium-site .env.example: template with placeholder values.
  • netvuln-tool templates/report_config.conf.example: the scanner config template.

Apache-2.0 licensed (appliance subtree proprietary)