Self-Hosted Licensing — HenKaiPan ASPM
This guide covers how licensing works for self-hosted instances: what's free, what's paid, how to generate and apply license keys, and how feature gating works under the hood.
Overview
HenKaiPan uses an offline license key model. No phone-home, no internet required after setup. The license is a signed JWT-like token validated locally by the API server.
| Mode | License Key | What you get |
|---|---|---|
| Free | Not set | Core scanning, findings triage, projects, webhooks |
| Paid | LICENSE_KEY set |
All features unlocked |
How it works
┌──────────────┐ LICENSE_KEY env var ┌────────────────────┐
│ License Key │─────────────────────────────▶│ API Server │
│ (HMAC-SHA256 │ │ ┌──────────────┐ │
│ signed JWT) │ │ │ license.Service│ │
└──────────────┘ │ │ │ │
│ │ - validates │ │
┌──────────────┐ GET /api/license │ │ - checks │ │
│ Frontend │◀────────────────────────────▶│ │ features │ │
│ Settings UI │ 402 Payment Required │ └──────────────┘ │
└──────────────┘ └────────────────────┘Feature Comparison
Free Tier (no license key)
| Feature | Status |
|---|---|
| Projects | Unlimited |
| Users | Unlimited |
| Scanners (SAST, SCA, Secrets, IaC, Containers) | All included |
| Manual scans | ✅ |
| Findings list, filter, triage | ✅ |
| Finding SLA tracking | ✅ |
| Dashboard (summary, SLA compliance) | ✅ |
| Knowledge base (read) | ✅ |
| Vulnerabilities view | ✅ |
| Webhooks | ✅ |
| Scan coverage reports | ✅ |
| Apps (business grouping) | ✅ |
Paid Features (require license key)
| Feature | Flag | API Routes |
|---|---|---|
| Scan scheduling (cron) | scheduling |
/api/schedules* |
| Policies & auto-triage | policies |
/api/policies*, /api/suppressions* |
| Compliance frameworks | compliance |
Frontend page |
| Integrations (Jira, GitHub, Slack) | integrations |
/api/integrations/jira*, /api/findings/*/jira* |
| AI remediation | ai-remediation |
/api/knowledge/ai-remediate, /api/findings/*/analyze |
| Reports & advanced metrics | reports |
/api/metrics/trends, /api/metrics/risk, /api/findings/export |
| Audit log | audit-log |
/api/audit-logs |
| Risk acceptance workflow | risk-acceptance |
/api/risk-acceptances*, /api/findings/*/risk-acceptance |
| Teams | teams |
/api/teams* |
| Finding comments | comments |
/api/findings/*/comments* |
| Email notifications | email-notifications |
/api/settings/notifications* |
Setup: Applying a License Key
1. Get a license key
Contact your account representative or generate one yourself (see Generating License Keys).
2. Set the environment variable
Add to your .env file:
LICENSE_KEY=HENKAI...base64-encoded-key...
LICENSE_SIGNING_SECRET=your-signing-secretIMPORTANT:
LICENSE_SIGNING_SECRETis required and must match the secret used to generate the key. Without it, no license key can be validated.
3. Restart the API
docker compose restart api4. Verify
Check the license status:
curl -H "Authorization: Bearer $(your-token)" http://localhost:8080/api/licenseOr view it in the UI: Settings → License.
Generating License Keys
Use the scripts/generate-license.sh script, which produces keys compatible with the API's offline validation.
You need a signing secret. Set it via
-sflag orLICENSE_SIGNING_SECRETenv var. Without it, the script errors out.
Basic usage
# Generate a 365-day key with all paid features
./scripts/generate-license.sh customer@example.com 365 -s "your-secret" \
-f "scheduling,policies,compliance,integrations,ai-remediation,reports,audit-log,risk-acceptance,teams,comments,email-notifications"Arguments
| Arg | Description | Default |
|---|---|---|
email |
License holder email (required) | — |
days |
Validity period in days | 365 |
-s |
HMAC signing secret (required) | $LICENSE_SIGNING_SECRET env var |
-f |
Comma-separated feature flags | empty (no paid features) |
Available feature flags
scheduling, policies, compliance, integrations, ai-remediation,
reports, audit-log, risk-acceptance, teams, comments, email-notificationsExamples
# Single feature
./scripts/generate-license.sh user@example.com 90 -s "mysecret" -f "scheduling"
# Subset of features
./scripts/generate-license.sh partner@example.com 180 -s "mysecret" \
-f "scheduling,policies,compliance"
# Using env var for secret
export LICENSE_SIGNING_SECRET=mysecret
./scripts/generate-license.sh admin@example.com 365 \
-f "scheduling,policies,compliance,integrations"Output
The script prints the key to stdout with instructions:
HenKaiPan ASPM License Key
==========================
Email: customer@example.com
Valid: 365 days (expires 2027-05-02)
Features: scheduling, policies, compliance, integrations, ...
License Key:
------------
eyJlbWFpbCI6...c2lnbmF0dXJlCg==
Set this key as LICENSE_KEY environment variable:
export LICENSE_KEY=eyJlbWFpbCI6...c2lnbmF0dXJlCg==Architecture
Validation (offline, no phone-home)
License Key (base64)
│
▼
Base64 decode ───► payload.signature
│
┌─────────────┴─────────────┐
▼ ▼
JSON payload HMAC-SHA256(payload, secret)
{email, expiry, Compare with signature
features:[...]}
│
▼
Valid? Expired? Features match?The license is validated entirely offline. The API never makes an external call. Each request to a paid endpoint is checked via chi middleware.
Feature gating
Routes for paid features are wrapped with licSvc.RequireFeature("feature-name"):
// Example from cmd/api/main.go
r.With(licSvc.RequireFeature(license.FeatureScheduling)).Group(func(r chi.Router) {
r.Get("/api/schedules", h.ListSchedules)
r.Post("/api/schedules", h.CreateSchedule)
// ...
})When a feature is not licensed:
- GET requests from admin/analyst users pass through (so the UI can render navigation and forms)
- All other requests return
402 Payment Requiredwith:
{
"error": "license_required",
"message": "This feature requires a paid license key. Contact sales@dyallab.com.ar to upgrade.",
"feature": "scheduling"
}Key format
The license key is a base64-encoded payload and HMAC-SHA256 signature joined by a .:
base64(json_payload . hmac_sha256(json_payload, secret))Payload schema:
{
"email": "customer@example.com",
"expiry": 1817424000,
"features": ["scheduling", "policies"]
}Security Notes
LICENSE_SIGNING_SECRETis required. There is no default. Without it, no license key can be validated.- Keep the signing secret safe. Anyone with access to it can generate valid license keys.
- License keys are not tied to a specific instance. A key can be shared — the trust model is that paying customers won't.
FAQ
Q: What happens if my license expires?
A: The API logs a warning on startup, returns expired status from /api/license, and paid endpoints return 402. The free tier continues working normally. Generate a new key with a later expiry and restart.
Q: Can I change features on an existing key?
A: Yes — generate a new key with the desired features and update LICENSE_KEY in the environment.
Q: Does the app phone home? A: No. License validation is 100% offline. There is no telemetry.
Q: What's the LICENSE_SIGNING_SECRET default?
A: There is no default. It must be explicitly set. If not set, no license key can be validated and the app runs in free mode only.
Q: Can I run without any license key? A: Yes. The app starts in free mode with all free features available. No license key is required.
File Reference
| File | Purpose |
|---|---|
internal/license/features.go |
Feature flag constants |
internal/license/license.go |
Service: parse, validate, HasFeature(), Status() |
internal/license/middleware.go |
RequireFeature() chi middleware |
internal/handlers/license.go |
GET /api/license handler |
cmd/api/main.go |
Route registration with license gating |
scripts/generate-license.sh |
CLI tool for key generation |
.env.example |
Config reference |