- Added a test case in CrowdSecConfig to show improved error message when preset is not cached. - Introduced a new test suite for the Dashboard component, verifying counts and health status. - Updated SMTPSettings tests to utilize a shared render function and added tests for backend validation errors. - Modified Security.audit tests to improve input handling and removed redundant export failure test. - Refactored Security tests to remove export functionality and ensure correct rendering of components. - Enhanced UsersPage tests with new scenarios for updating user permissions and manual invite link flow. - Created a new utility for rendering components with a QueryClient and MemoryRouter for better test isolation. - Updated go-test-coverage script to improve error handling and coverage reporting.
20 KiB
API Documentation
Charon REST API documentation. All endpoints return JSON and use standard HTTP status codes.
Base URL
http://localhost:8080/api/v1
Authentication
🚧 Authentication is not yet implemented. All endpoints are currently public.
Future authentication will use JWT tokens:
Authorization: Bearer <token>
Response Format
Success Response
{
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"name": "Example",
"created_at": "2025-01-18T10:00:00Z"
}
Error Response
{
"error": "Resource not found",
"code": 404
}
Status Codes
| Code | Description |
|---|---|
| 200 | Success |
| 201 | Created |
| 204 | No Content (successful deletion) |
| 400 | Bad Request (validation error) |
| 404 | Not Found |
| 500 | Internal Server Error |
Endpoints
Metrics (Prometheus)
Expose internal counters for scraping.
GET /metrics
No authentication required. Primary WAF metrics:
charon_waf_requests_total
charon_waf_blocked_total
charon_waf_monitored_total
Health Check
Check API health status.
GET /health
Response 200:
{
"status": "ok"
}
Security Suite (Cerberus)
Status
GET /security/status
Returns enabled flag plus modes for each module.
Get Global Security Config
GET /security/config
Response 200 (no config yet): { "config": null }
Upsert Global Security Config
POST /security/config
Content-Type: application/json
Request Body (example):
{
"name": "default",
"enabled": true,
"admin_whitelist": "198.51.100.10,203.0.113.0/24",
"crowdsec_mode": "local",
"waf_mode": "monitor",
"waf_rules_source": "owasp-crs-local"
}
Response 200: { "config": { ... } }
Enable Cerberus
POST /security/enable
Payload (optional break-glass token):
{ "break_glass_token": "abcd1234" }
Disable Cerberus
POST /security/disable
Payload (required if not localhost):
{ "break_glass_token": "abcd1234" }
Generate Break-Glass Token
POST /security/breakglass/generate
Response 200: { "token": "plaintext-token-once" }
List Security Decisions
GET /security/decisions?limit=50
Response 200: { "decisions": [ ... ] }
Create Manual Decision
POST /security/decisions
Content-Type: application/json
Payload:
{ "ip": "203.0.113.5", "action": "block", "details": "manual temporary block" }
List Rulesets
GET /security/rulesets
Response 200: { "rulesets": [ ... ] }
Upsert Ruleset
POST /security/rulesets
Content-Type: application/json
Payload:
{
"name": "owasp-crs-quick",
"source_url": "https://example.com/owasp-crs.txt",
"mode": "owasp",
"content": "# raw rules"
}
Response 200: { "ruleset": { ... } }
Delete Ruleset
DELETE /security/rulesets/:id
Response 200: { "deleted": true }
SSL Certificates
List All Certificates
GET /certificates
Response 200:
[
{
"id": 1,
"uuid": "cert-uuid-123",
"name": "My Custom Cert",
"provider": "custom",
"domains": "example.com, www.example.com",
"expires_at": "2026-01-01T00:00:00Z",
"created_at": "2025-01-01T10:00:00Z"
}
]
Upload Certificate
POST /certificates/upload
Content-Type: multipart/form-data
Request Body:
name(required) - Certificate namecertificate_file(required) - Certificate file (.crt or .pem)key_file(required) - Private key file (.key or .pem)
Response 201:
{
"id": 1,
"uuid": "cert-uuid-123",
"name": "My Custom Cert",
"provider": "custom",
"domains": "example.com"
}
Delete Certificate
Delete a certificate. Requires that the certificate is not currently in use by any proxy hosts.
DELETE /certificates/:id
Parameters:
id(path) - Certificate ID (numeric)
Response 200:
{
"message": "certificate deleted"
}
Response 400:
{
"error": "invalid id"
}
Response 409:
{
"error": "certificate is in use by one or more proxy hosts"
}
Response 500:
{
"error": "failed to delete certificate"
}
Note: A backup is automatically created before deletion. The certificate files are removed from disk along with the database record.
Proxy Hosts
List All Proxy Hosts
GET /proxy-hosts
Response 200:
[
{
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"domain": "example.com, www.example.com",
"forward_scheme": "http",
"forward_host": "localhost",
"forward_port": 8080,
"ssl_forced": false,
"http2_support": true,
"hsts_enabled": false,
"hsts_subdomains": false,
"block_exploits": true,
"websocket_support": false,
"enabled": true,
"remote_server_id": null,
"created_at": "2025-01-18T10:00:00Z",
"updated_at": "2025-01-18T10:00:00Z"
}
]
Get Proxy Host
GET /proxy-hosts/:uuid
Parameters:
uuid(path) - Proxy host UUID
Response 200:
{
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"domain": "example.com",
"forward_scheme": "https",
"forward_host": "backend.internal",
"forward_port": 9000,
"ssl_forced": true,
"websocket_support": false,
"enabled": true,
"created_at": "2025-01-18T10:00:00Z",
"updated_at": "2025-01-18T10:00:00Z"
}
Response 404:
{
"error": "Proxy host not found"
}
Create Proxy Host
POST /proxy-hosts
Content-Type: application/json
Request Body:
{
"domain": "new.example.com",
"forward_scheme": "http",
"forward_host": "localhost",
"forward_port": 3000,
"ssl_forced": false,
"http2_support": true,
"hsts_enabled": false,
"hsts_subdomains": false,
"block_exploits": true,
"websocket_support": false,
"enabled": true,
"remote_server_id": null
}
Required Fields:
domain- Domain name(s), comma-separatedforward_host- Target hostname or IPforward_port- Target port number
Optional Fields:
forward_scheme- Default:"http"ssl_forced- Default:falsehttp2_support- Default:truehsts_enabled- Default:falsehsts_subdomains- Default:falseblock_exploits- Default:truewebsocket_support- Default:falseenabled- Default:trueremote_server_id- Default:null
Response 201:
{
"uuid": "550e8400-e29b-41d4-a716-446655440001",
"domain": "new.example.com",
"forward_scheme": "http",
"forward_host": "localhost",
"forward_port": 3000,
"created_at": "2025-01-18T10:05:00Z",
"updated_at": "2025-01-18T10:05:00Z"
}
Response 400:
{
"error": "domain is required"
}
Update Proxy Host
PUT /proxy-hosts/:uuid
Content-Type: application/json
Parameters:
uuid(path) - Proxy host UUID
Request Body: (all fields optional)
{
"domain": "updated.example.com",
"forward_port": 8081,
"ssl_forced": true
}
Response 200:
{
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"domain": "updated.example.com",
"forward_port": 8081,
"ssl_forced": true,
"updated_at": "2025-01-18T10:10:00Z"
}
Delete Proxy Host
DELETE /proxy-hosts/:uuid
Parameters:
uuid(path) - Proxy host UUID
Response 204: No content
Response 404:
{
"error": "Proxy host not found"
}
Remote Servers
List All Remote Servers
GET /remote-servers
Query Parameters:
enabled(optional) - Filter by enabled status (trueorfalse)
Response 200:
[
{
"uuid": "660e8400-e29b-41d4-a716-446655440000",
"name": "Docker Registry",
"provider": "docker",
"host": "registry.local",
"port": 5000,
"reachable": true,
"last_checked": "2025-01-18T09:55:00Z",
"enabled": true,
"created_at": "2025-01-18T09:00:00Z",
"updated_at": "2025-01-18T09:55:00Z"
}
]
Get Remote Server
GET /remote-servers/:uuid
Parameters:
uuid(path) - Remote server UUID
Response 200:
{
"uuid": "660e8400-e29b-41d4-a716-446655440000",
"name": "Docker Registry",
"provider": "docker",
"host": "registry.local",
"port": 5000,
"reachable": true,
"enabled": true
}
Create Remote Server
POST /remote-servers
Content-Type: application/json
Request Body:
{
"name": "Production API",
"provider": "generic",
"host": "api.prod.internal",
"port": 8080,
"enabled": true
}
Required Fields:
name- Server namehost- Hostname or IPport- Port number
Optional Fields:
provider- One of:generic,docker,kubernetes,aws,gcp,azure(default:generic)enabled- Default:true
Response 201:
{
"uuid": "660e8400-e29b-41d4-a716-446655440001",
"name": "Production API",
"provider": "generic",
"host": "api.prod.internal",
"port": 8080,
"reachable": false,
"enabled": true,
"created_at": "2025-01-18T10:15:00Z"
}
Update Remote Server
PUT /remote-servers/:uuid
Content-Type: application/json
Request Body: (all fields optional)
{
"name": "Updated Name",
"port": 8081,
"enabled": false
}
Response 200:
{
"uuid": "660e8400-e29b-41d4-a716-446655440000",
"name": "Updated Name",
"port": 8081,
"enabled": false,
"updated_at": "2025-01-18T10:20:00Z"
}
Delete Remote Server
DELETE /remote-servers/:uuid
Response 204: No content
Test Remote Server Connection
Test connectivity to a remote server.
POST /remote-servers/:uuid/test
Parameters:
uuid(path) - Remote server UUID
Response 200:
{
"reachable": true,
"address": "registry.local:5000",
"timestamp": "2025-01-18T10:25:00Z"
}
Response 200 (unreachable):
{
"reachable": false,
"address": "offline.server:8080",
"error": "connection timeout",
"timestamp": "2025-01-18T10:25:00Z"
}
Note: This endpoint updates the reachable and last_checked fields on the remote server.
Live Logs & Notifications
Stream Live Logs (WebSocket)
Connect to a WebSocket stream of live security logs. This endpoint uses WebSocket protocol for real-time bidirectional communication.
GET /api/v1/logs/live
Upgrade: websocket
Query Parameters:
level(optional) - Filter by log level. Values:debug,info,warn,errorsource(optional) - Filter by log source. Values:cerberus,waf,crowdsec,acl
WebSocket Connection:
const ws = new WebSocket('ws://localhost:8080/api/v1/logs/live?source=cerberus&level=error');
ws.onmessage = (event) => {
const logEntry = JSON.parse(event.data);
console.log(logEntry);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
ws.onclose = () => {
console.log('Connection closed');
};
Message Format:
Each message received from the WebSocket is a JSON-encoded LogEntry:
{
"level": "error",
"message": "WAF blocked request from 203.0.113.42",
"timestamp": "2025-12-09T10:30:45Z",
"source": "waf",
"fields": {
"ip": "203.0.113.42",
"rule_id": "942100",
"request_uri": "/api/users?id=1' OR '1'='1",
"severity": "CRITICAL"
}
}
Field Descriptions:
level- Log severity:debug,info,warn,errormessage- Human-readable log messagetimestamp- ISO 8601 timestamp (RFC3339 format)source- Component that generated the log (e.g.,cerberus,waf,crowdsec)fields- Additional structured data specific to the event type
Connection Lifecycle:
- Server sends a ping every 30 seconds to keep connection alive
- Client should respond to pings or connection may timeout
- Server closes connection if client stops reading
- Client can close connection by calling
ws.close()
Error Handling:
- If upgrade fails, returns HTTP 400 with error message
- Authentication required (when auth is implemented)
- Rate limiting applies (when rate limiting is implemented)
Example: Filter for critical WAF events only
const ws = new WebSocket('ws://localhost:8080/api/v1/logs/live?source=waf&level=error');
Get Notification Settings
Retrieve current security notification settings.
GET /api/v1/security/notifications/settings
Response 200:
{
"enabled": true,
"min_log_level": "warn",
"notify_waf_blocks": true,
"notify_acl_denials": true,
"notify_rate_limit_hits": false,
"webhook_url": "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXX",
"email_recipients": "admin@example.com,security@example.com"
}
Field Descriptions:
enabled- Master toggle for all notificationsmin_log_level- Minimum severity to trigger notifications. Values:debug,info,warn,errornotify_waf_blocks- Send notifications for WAF blocking eventsnotify_acl_denials- Send notifications for ACL denial eventsnotify_rate_limit_hits- Send notifications for rate limit violationswebhook_url(optional) - URL to POST webhook notifications (Discord, Slack, etc.)email_recipients(optional) - Comma-separated list of email addresses
Response 404:
{
"error": "Notification settings not configured"
}
Update Notification Settings
Update security notification settings. All fields are optional—only provided fields are updated.
PUT /api/v1/security/notifications/settings
Content-Type: application/json
Request Body:
{
"enabled": true,
"min_log_level": "error",
"notify_waf_blocks": true,
"notify_acl_denials": false,
"notify_rate_limit_hits": false,
"webhook_url": "https://discord.com/api/webhooks/123456789/abcdefgh",
"email_recipients": "alerts@example.com"
}
All fields optional:
enabled(boolean) - Enable/disable all notificationsmin_log_level(string) - Must be one of:debug,info,warn,errornotify_waf_blocks(boolean) - Toggle WAF block notificationsnotify_acl_denials(boolean) - Toggle ACL denial notificationsnotify_rate_limit_hits(boolean) - Toggle rate limit notificationswebhook_url(string) - Webhook endpoint URLemail_recipients(string) - Comma-separated email addresses
Response 200:
{
"message": "Settings updated successfully"
}
Response 400:
{
"error": "Invalid min_log_level. Must be one of: debug, info, warn, error"
}
Response 500:
{
"error": "Failed to update settings"
}
Example: Enable notifications for critical errors only
curl -X PUT http://localhost:8080/api/v1/security/notifications/settings \
-H "Content-Type: application/json" \
-d '{
"enabled": true,
"min_log_level": "error",
"notify_waf_blocks": true,
"webhook_url": "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
}'
Webhook Payload Format:
When notifications are triggered, Charon sends a POST request to the configured webhook URL:
{
"event_type": "waf_block",
"severity": "error",
"timestamp": "2025-12-09T10:30:45Z",
"message": "WAF blocked SQL injection attempt",
"details": {
"ip": "203.0.113.42",
"rule_id": "942100",
"request_uri": "/api/users?id=1' OR '1'='1",
"user_agent": "curl/7.68.0"
}
}
Import Workflow
Check Import Status
Check if there's an active import session.
GET /import/status
Response 200 (no session):
{
"has_pending": false
}
Response 200 (active session):
{
"has_pending": true,
"session": {
"uuid": "770e8400-e29b-41d4-a716-446655440000",
"filename": "Caddyfile",
"state": "reviewing",
"created_at": "2025-01-18T10:30:00Z",
"updated_at": "2025-01-18T10:30:00Z"
}
}
Get Import Preview
Get preview of hosts to be imported (only available when session state is reviewing).
GET /import/preview
Response 200:
{
"hosts": [
{
"domain": "example.com",
"forward_host": "localhost",
"forward_port": 8080,
"forward_scheme": "http"
},
{
"domain": "api.example.com",
"forward_host": "backend",
"forward_port": 9000,
"forward_scheme": "https"
}
],
"conflicts": [
"example.com already exists"
],
"errors": []
}
Response 404:
{
"error": "No active import session"
}
Upload Caddyfile
Upload a Caddyfile for import.
POST /import/upload
Content-Type: application/json
Request Body:
{
"content": "example.com {\n reverse_proxy localhost:8080\n}",
"filename": "Caddyfile"
}
Required Fields:
content- Caddyfile content
Optional Fields:
filename- Original filename (default:"Caddyfile")
Response 201:
{
"session": {
"uuid": "770e8400-e29b-41d4-a716-446655440000",
"filename": "Caddyfile",
"state": "parsing",
"created_at": "2025-01-18T10:35:00Z"
}
}
Response 400:
{
"error": "content is required"
}
Commit Import
Commit the import after resolving conflicts.
POST /import/commit
Content-Type: application/json
Request Body:
{
"session_uuid": "770e8400-e29b-41d4-a716-446655440000",
"resolutions": {
"example.com": "overwrite",
"api.example.com": "keep"
}
}
Required Fields:
session_uuid- Active import session UUIDresolutions- Map of domain to resolution strategy
Resolution Strategies:
"keep"- Keep existing configuration, skip import"overwrite"- Replace existing with imported configuration"skip"- Same as keep
Response 200:
{
"imported": 2,
"skipped": 1,
"failed": 0
}
Response 400:
{
"error": "Invalid session or unresolved conflicts"
}
Cancel Import
Cancel an active import session.
DELETE /import/cancel?session_uuid=770e8400-e29b-41d4-a716-446655440000
Query Parameters:
session_uuid- Active import session UUID
Response 204: No content
Rate Limiting
🚧 Rate limiting is not yet implemented.
Future rate limits:
- 100 requests per minute per IP
- 1000 requests per hour per IP
Pagination
🚧 Pagination is not yet implemented.
Future pagination:
GET /proxy-hosts?page=1&per_page=20
Filtering and Sorting
🚧 Advanced filtering is not yet implemented.
Future filtering:
GET /proxy-hosts?enabled=true&sort=created_at&order=desc
Webhooks
🚧 Webhooks are not yet implemented.
Future webhook events:
proxy_host.createdproxy_host.updatedproxy_host.deletedremote_server.unreachableimport.completed
SDKs
No official SDKs yet. The API follows REST conventions and can be used with any HTTP client.
JavaScript/TypeScript Example
const API_BASE = 'http://localhost:8080/api/v1';
// List proxy hosts
const hosts = await fetch(`${API_BASE}/proxy-hosts`).then(r => r.json());
// Create proxy host
const newHost = await fetch(`${API_BASE}/proxy-hosts`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
domain: 'example.com',
forward_host: 'localhost',
forward_port: 8080
})
}).then(r => r.json());
// Test remote server
const testResult = await fetch(`${API_BASE}/remote-servers/${uuid}/test`, {
method: 'POST'
}).then(r => r.json());
Python Example
import requests
API_BASE = 'http://localhost:8080/api/v1'
# List proxy hosts
hosts = requests.get(f'{API_BASE}/proxy-hosts').json()
# Create proxy host
new_host = requests.post(f'{API_BASE}/proxy-hosts', json={
'domain': 'example.com',
'forward_host': 'localhost',
'forward_port': 8080
}).json()
# Test remote server
test_result = requests.post(f'{API_BASE}/remote-servers/{uuid}/test').json()
Support
For API issues or questions:
- GitHub Issues: https://github.com/Wikid82/charon/issues
- Discussions: https://github.com/Wikid82/charon/discussions