Appearance
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)
| Secret | Env Var | Where Set | Scope | Risk if Leaked |
|---|---|---|---|---|
| Neon PostgreSQL URL | NEON_DATABASE_URL | Netlify dashboard (per-branch context) | DB read/write for all session metadata | CRITICAL: full DB access |
| Neon PostgreSQL URL (fallback) | NETLIFY_DATABASE_URL | Netlify dashboard | Same as above (alternative name) | CRITICAL |
| Legacy upload API key | NV_UPLOAD_API_KEY | Netlify dashboard | Authenticates CLI uploads (legacy path) | HIGH: upload access |
| Plausible API key | PLAUSIBLE_API_KEY | Local .env only | Read-only analytics queries | LOW: analytics data only |
| Neon management API key | NEON_DB_KEY | Local .env only | Neon dashboard API (branch mgmt) | MEDIUM: DB admin ops |
| GitLab personal access token | GITLAB_TOKEN | Local .env only | GitLab API for CI/MR automation | HIGH: repo write access |
| Netlify auth token | NETLIFY_AUTH_TOKEN | Local .env only (migration) | Netlify API for blob migration | MEDIUM: 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)
| Secret | Config Var | Where Set | Scope | Risk if Leaked |
|---|---|---|---|---|
| Portal API URL | CONSULTATION_API_URL | report_config.conf | Upload endpoint | LOW: URL only, not a secret |
| License key (current) | NV_API_KEY | report_config.conf or env var | Upload auth, tier-gated | HIGH: upload + tier access |
| Legacy API key | CONSULTATION_API_KEY | report_config.conf | Same as NV_API_KEY (deprecated alias) | HIGH |
| Enterprise license | NV_LICENSE_KEY | Env var or ~/.netvuln/license.key | Feature gating | LOW: no remote access |
| Slack webhook URL | ALERT_SLACK_WEBHOOK | report_config.conf | Post messages to a Slack channel | MEDIUM: spam risk |
| Alert webhook URL | ALERT_WEBHOOK_URL | report_config.conf | POST to an external webhook | MEDIUM |
| Alert email | ALERT_EMAIL | report_config.conf | Send email alerts | LOW |
| License cache | n/a | ~/.netvuln/license_cache.json (mode 600) | Online validation cache | NONE: 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
| Secret | Platform | Where Set | Used By |
|---|---|---|---|
STAGING_API_URL | Gitea Actions | Repository secrets | E2E tests (e2e.yml) |
STAGING_API_KEY | Gitea Actions | Repository secrets | E2E tests (e2e.yml) |
Environment Setup
bullium-site: Local Development
Copy the template:
bashcp .env.example .envPopulate 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-xxxRun the local dev server:
bashnpx netlify dev --port 8888Netlify Dev automatically loads
.envand injects the vars into functions.
bullium-site: Netlify Dashboard Configuration
Environment variables are set per-context in the Netlify dashboard:
| Context | NEON_DATABASE_URL | NV_UPLOAD_API_KEY | Notes |
|---|---|---|---|
| Production (main) | Points to Neon main branch | Production upload key | Auto-deploys on merge |
| Dev branch deploy | Points to Neon dev branch | Same or test key | Schema validation first |
| Deploy preview | Points to Neon dev branch | Test key | PR 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
Copy the config template:
bashcp templates/report_config.conf.example ~/netvuln-tool/configs/my-client.confPopulate 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"Verify connectivity:
bashbash 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_KEYThe 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.
| Environment | Neon Branch | How to identify |
|---|---|---|
Local dev (netlify dev) | dev | URL contains the ep-xxx dev endpoint |
| Netlify dev deploy | dev | Set in the Netlify dashboard context |
| Deploy preview | dev | Set in the Netlify dashboard context |
| Production | main | Set 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:
| Purpose | Key Source | Notes |
|---|---|---|
| Local testing | Generate a test license key via the admin panel | Lower tier (professional) is fine |
| CI/CD E2E tests | STAGING_API_KEY in Gitea Actions secrets | Points at the dev deploy |
| Client production | Per-client license key via the admin panel | Tier matches their subscription |
| Legacy fallback | NV_UPLOAD_API_KEY env var in Netlify | Single key, no per-client scoping |
Key Rotation Schedule
When to Rotate
| Trigger | Action | Urgency |
|---|---|---|
| Suspected compromise | Rotate immediately, audit access logs | IMMEDIATE |
| Team member leaves | Rotate all keys they had access to | Within 24 hours |
| Key found in logs/settings | Rotate the exposed key | Within 24 hours |
| Routine maintenance | Rotate highest-risk keys | Quarterly |
| Netlify Identity user removed | No 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:
- Generate a new key in the admin panel (Licenses tab, Create)
- Same tier and client_id as the old key
- Update scanner configs that use the old key
- Edit
NV_API_KEYin each affectedreport_config.conf - If the daemon is running:
netvuln_daemon_ctl.sh reload(picks up the new config)
- Edit
- 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
- 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:
- Go to the Neon dashboard, then Project, then Connection details
- Reset the password for the relevant branch (dev or main)
- Copy the new connection string
- Update it in the Netlify dashboard (Settings, Environment Variables, edit the context-specific
NEON_DATABASE_URL) - Update local
.envif you changed the branch you use locally - 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
- Go to the Plausible dashboard, then Settings, then API Keys
- Revoke the old key, create a new one
- Update the local
.envfile:PLAUSIBLE_API_KEY=new-key-here - 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
- Go to GitLab, then Preferences, then Access Tokens
- Revoke the old token, create a new one with the same scopes
- Update the local
.envfile: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
- Go to Gitea, then Settings, then Applications, then Access Tokens
- Delete the old token, create a new one
- Update any scripts or settings files referencing it
- Clean the old token from
.claude/settings.local.jsoncommand history
Secret Hygiene Checklist
Things That Should NEVER Contain Real Secrets
| Location | Why It's Risky | What to Do |
|---|---|---|
| Git history | Permanent, even after deletion | Use .gitignore, never commit .env |
.claude/settings.local.json | Accumulates approved commands with tokens | Periodically audit and clean |
Shell history (~/.bash_history) | Plaintext command log | Use env vars, not inline tokens |
| CI/CD logs | May be publicly readable | Never echo secrets, use --silent in curl |
| Screenshots | Easy to share accidentally | Redact before sharing |
| Slack/email | Persisted in third-party systems | Use 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.jsontests/e2e/e2e.conf- All
*.conffiles (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.jsonArchitecture 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:
- Netlify dashboard handles production secrets (encrypted at rest, per-context)
- Neon branching separates dev from prod at the database level
- License keys provide per-client isolation with tier-based permissions
.gitignoreplusnetlify.tomlforced 404s provide two-layer exposure prevention- 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):
NEON_DATABASE_URL: full DB access, most criticalNV_UPLOAD_API_KEY/ license keys: upload authenticationGITLAB_TOKEN: repo write accessPLAUSIBLE_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:
- HashiCorp Learn: https://developer.hashicorp.com/vault/tutorials
- Doppler docs: https://docs.doppler.com
- OWASP Secrets Management Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html
Related Documentation
- 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.
