224 lines
6.8 KiB
Markdown
224 lines
6.8 KiB
Markdown
# Clavitor Edition System Specification
|
|
|
|
## Overview
|
|
|
|
Clavitor Vault has two editions with **build-time separation** — not runtime flags, not config files, not license keys. The edition is determined at compile time via Go build tags.
|
|
|
|
| Edition | Build Tag | Target User |
|
|
|---------|-----------|-------------|
|
|
| **Community** | (none, default) | Self-hosted individuals, privacy-first users |
|
|
| **Commercial** | `-tags commercial` | Managed service, enterprise, multi-POP deployments |
|
|
|
|
## Architecture
|
|
|
|
```
|
|
edition/
|
|
├── edition.go # Interface + shared types (always compiled)
|
|
├── community.go # Community implementation (go:build !commercial)
|
|
└── commercial.go # Commercial implementation (go:build commercial)
|
|
```
|
|
|
|
The `edition.Current` variable is set at `init()` time based on build tags. Application code never checks build tags directly — it uses `edition.Current.Name()` or calls methods on the interface.
|
|
|
|
## Build Commands
|
|
|
|
```bash
|
|
# Community Edition (default) - Elastic 2, self-hosted, no external calls
|
|
go build -o clavitor ./cmd/clavitor/
|
|
|
|
# Commercial Edition - managed service, telemetry, replication
|
|
go build -tags commercial -o clavitor ./cmd/clavitor/
|
|
```
|
|
|
|
## The Edition Interface
|
|
|
|
```go
|
|
type Edition interface {
|
|
// Name returns "community" or "commercial"
|
|
Name() string
|
|
|
|
// AlertOperator sends critical operational alerts
|
|
// Community: logs to stderr
|
|
// Commercial: POSTs to telemetry endpoint + logs
|
|
AlertOperator(ctx context.Context, alertType, message string, details map[string]any)
|
|
|
|
// IsTelemetryEnabled returns true if data goes to central servers
|
|
IsTelemetryEnabled() bool
|
|
|
|
// NotFoundHandler handles 404/405 routes
|
|
// Community: simple 404 (fast, clean shutdown)
|
|
// Commercial: tarpit (anti-scanner, interruptible on shutdown)
|
|
NotFoundHandler() http.HandlerFunc
|
|
}
|
|
```
|
|
|
|
## Feature Matrix
|
|
|
|
| Feature | Community | Commercial |
|
|
|---------|-----------|------------|
|
|
| **License** | Elastic License 2.0 | Commercial |
|
|
| **Telemetry** | None (unless user manually configures) | Enabled by default to clavitor.ai |
|
|
| **Operator Alerts** | Local logs only | POSTs to `/v1/alerts` endpoint |
|
|
| **Replication** | ❌ Not available | ✅ Real-time sync to backup POPs |
|
|
| **Tarpit** | ❌ Simple 404 | ✅ 30s anti-scanner drain |
|
|
| **Central Management** | ❌ None | ✅ Multi-POP dashboard |
|
|
| **SCIM/SIEM** | ❌ No | ✅ Yes |
|
|
|
|
## Commercial-Only Features (Detailed)
|
|
|
|
### 1. Real-Time Replication to Backup POPs
|
|
|
|
**File:** `edition/replication.go` (only compiled with `-tags commercial`)
|
|
|
|
Background worker that:
|
|
- Polls `EntryListUnreplicated()` for dirty entries
|
|
- POSTs encrypted entry blobs to backup POP (Calgary/Zurich)
|
|
- Automatic retry with exponential backoff
|
|
- Batching (up to 100 entries per request)
|
|
- Conflict resolution: last-write-wins
|
|
|
|
**Community:** No replication code exists in binary. `ReplicatedAt` field in DB is never used.
|
|
|
|
### 2. Tarpit (Anti-Scanner Defense)
|
|
|
|
**File:** `edition/commercial.go` → `NotFoundHandler()`
|
|
|
|
For 404/405 responses:
|
|
- Holds connection for 30 seconds
|
|
- Drips one byte per second
|
|
- Caps at 1000 concurrent tarpit connections
|
|
- **Interruptible:** Checks `edition.ShutdownContext` each iteration
|
|
|
|
**Community:** Returns HTTP 404 immediately. Clean, fast shutdown.
|
|
|
|
### 3. Centralized Telemetry & Alerting
|
|
|
|
**Files:** `edition/commercial.go`
|
|
|
|
- Periodic metrics POST to `TelemetryHost`
|
|
- Operator alerts POST to `/v1/alerts`
|
|
- Configured via `CommercialConfig` struct
|
|
|
|
**Community:** `AlertOperator()` logs locally only. No network calls.
|
|
|
|
## Code Patterns
|
|
|
|
### Checking Edition
|
|
|
|
```go
|
|
// DON'T do this:
|
|
// if edition.Current.Name() == "commercial" { ... }
|
|
|
|
// DO this - use interface methods:
|
|
edition.Current.AlertOperator(ctx, "error", "message", details)
|
|
```
|
|
|
|
The interface abstracts the behavior. Don't branch on the name unless absolutely necessary (e.g., UI hints).
|
|
|
|
### Adding Commercial-Only Features
|
|
|
|
1. **Define behavior in interface** (`edition/edition.go`):
|
|
```go
|
|
type Edition interface {
|
|
// ... existing methods ...
|
|
NewFeature() SomeType
|
|
}
|
|
```
|
|
|
|
2. **Implement in community** (`edition/community.go`):
|
|
```go
|
|
func (e *communityEdition) NewFeature() SomeType {
|
|
// Return no-op or simple implementation
|
|
return nil
|
|
}
|
|
```
|
|
|
|
3. **Implement in commercial** (`edition/commercial.go`):
|
|
```go
|
|
func (e *commercialEdition) NewFeature() SomeType {
|
|
// Full implementation
|
|
return &commercialFeature{...}
|
|
}
|
|
```
|
|
|
|
4. **Use in application code**:
|
|
```go
|
|
feature := edition.Current.NewFeature()
|
|
if feature != nil {
|
|
feature.DoSomething()
|
|
}
|
|
```
|
|
|
|
## Graceful Shutdown
|
|
|
|
Both editions support graceful shutdown via `edition.ShutdownContext`:
|
|
|
|
```go
|
|
// main.go sets this on startup
|
|
edition.ShutdownContext = shutdownCtx
|
|
|
|
// Commercial tarpit checks this:
|
|
select {
|
|
case <-edition.ShutdownContext.Done():
|
|
return // Exit immediately on SIGTERM
|
|
case <-time.After(time.Second):
|
|
// Continue dripping
|
|
}
|
|
```
|
|
|
|
**Community:** Shutdown is instant (no long-running handlers).
|
|
|
|
**Commercial:** Tarpit connections exit within 1 second of SIGTERM.
|
|
|
|
## Testing
|
|
|
|
```bash
|
|
# Test both editions
|
|
go test ./edition/...
|
|
go test -tags commercial ./edition/...
|
|
|
|
# Build verification
|
|
go build ./cmd/clavitor/ # Community
|
|
go build -tags commercial ./cmd/clavitor/ # Commercial
|
|
|
|
# Verify feature presence
|
|
strings /tmp/clavitor-community | grep -i tarpit # Should be empty
|
|
strings /tmp/clavitor-commercial | grep -i tarpit # Should find references
|
|
```
|
|
|
|
## Important Rules
|
|
|
|
1. **Never remove build tags** — they enforce the dual-license model
|
|
2. **Always use `edition.Current`** — no build tag checks in app code
|
|
3. **Community is default** — `go build` without tags must work
|
|
4. **Commercial config is optional** — commercial binary runs without config (just degraded)
|
|
5. **No runtime switching** — edition is baked into binary, not changeable
|
|
|
|
## FAQ for Agents
|
|
|
|
**Q: Why not use a config flag for community vs commercial?**
|
|
A: Build-time separation ensures:
|
|
- Community users cannot accidentally enable commercial features
|
|
- Commercial code (telemetry, replication) doesn't exist in Elastic 2 binary
|
|
- Clean mental model: you get what you build
|
|
|
|
**Q: Can a community user upgrade to commercial without rebuilding?**
|
|
A: No. They must rebuild with `-tags commercial` and configure `CommercialConfig`.
|
|
|
|
**Q: What happens if commercial code is called without config?**
|
|
A: Graceful degradation:
|
|
- Telemetry disabled (no host configured)
|
|
- Replication disabled (no config file)
|
|
- Alerts logged locally only
|
|
- Tarpit still works (needs no config)
|
|
|
|
**Q: How do I know which edition is running?**
|
|
A: Check logs at startup:
|
|
```
|
|
Starting Clavitor Vault v2.0.45 - Community Edition
|
|
# or
|
|
Starting Clavitor Vault v2.0.45 - Commercial Edition
|
|
```
|
|
|
|
Or call `edition.Current.Name()` programmatically.
|