Security
ClawDesk implements a defense-in-depth security architecture with four cascading layers. Every inbound message passes through all four layers before reaching the agent pipeline, and every outbound response is validated before delivery.
Security Architecture
Layer Summary
| Layer | Component | Purpose | Performance |
|---|---|---|---|
| 1. Allowlist | AllowlistManager | Fast identity-based filtering | O(1) |
| 2. Content Scanning | CascadeScanner | Multi-stage content analysis | O(n) |
| 3. Access Control | AclManager | Role-based permission checks | O(1) |
| 4. Audit | AuditLogger | Hash-chained audit trail | O(1) |
Layer 1: Allowlist
The allowlist is the first line of defense—a fast O(1) check that accepts or rejects messages based on sender identity before any further processing.
Configuration
[security.allowlist]
enabled = true
mode = "allowlist" # "allowlist" | "denylist" | "open"
# User allowlist
users = [
"telegram:12345678",
"discord:987654321",
"slack:U01234ABCDE",
"*:admin@company.com", # any channel
]
# Group/channel allowlist
groups = [
"telegram:-100123456789",
"discord:guild:123456789012345678",
"slack:channel:C01234ABCDE",
]
# Wildcard patterns
patterns = [
"telegram:*", # all Telegram users (use with caution)
"slack:U*", # all Slack users
"*:*@company.com", # all company emails across channels
]
Modes
| Mode | Behavior |
|---|---|
allowlist | Only listed identities can interact (default, most secure) |
denylist | Everyone except listed identities can interact |
open | No identity filtering (use with other security layers) |
AllowlistManager
pub struct AllowlistManager {
mode: AllowlistMode,
users: HashSet<IdentityPattern>,
groups: HashSet<IdentityPattern>,
patterns: Vec<GlobPattern>,
}
impl AllowlistManager {
/// Check if an identity is allowed
pub fn is_allowed(&self, identity: &Identity) -> bool {
match self.mode {
AllowlistMode::Open => true,
AllowlistMode::Allowlist => {
self.users.contains(&identity.to_pattern())
|| self.groups.contains(&identity.group_pattern())
|| self.patterns.iter().any(|p| p.matches(identity))
}
AllowlistMode::Denylist => {
!self.users.contains(&identity.to_pattern())
&& !self.groups.contains(&identity.group_pattern())
&& !self.patterns.iter().any(|p| p.matches(identity))
}
}
}
/// Add an identity to the allowlist
pub fn add(&mut self, pattern: &str) -> Result<()>;
/// Remove an identity from the allowlist
pub fn remove(&mut self, pattern: &str) -> Result<()>;
}
CLI Management
# View allowlist
clawdesk security allowlist show
# Add entries
clawdesk security allowlist add "telegram:12345678"
clawdesk security allowlist add "discord:987654321"
clawdesk security allowlist add "*:admin@company.com"
# Remove entries
clawdesk security allowlist remove "telegram:12345678"
# Test an identity
clawdesk security allowlist check "telegram:12345678"
# ✅ Allowed: matches rule "telegram:12345678"
Layer 2: Content Scanning
The CascadeScanner analyzes message content through three progressive scanning stages, from fastest to most thorough:
Stage 1: Regex Scanning
Fast pattern matching for known dangerous patterns:
[security.scanning.regex]
enabled = true
# Built-in patterns (always active)
# - SQL injection patterns
# - Shell command injection
# - Path traversal attempts
# - Credential patterns (API keys, tokens)
# Custom patterns
[[security.scanning.regex.patterns]]
name = "company_secrets"
pattern = '(?i)(internal\s+use\s+only|confidential|secret\s+project\s+\w+)'
action = "block" # "block" | "warn" | "redact"
message = "Message contains potentially confidential information"
[[security.scanning.regex.patterns]]
name = "pii_ssn"
pattern = '\b\d{3}-\d{2}-\d{4}\b'
action = "redact"
replacement = "[SSN REDACTED]"
[[security.scanning.regex.patterns]]
name = "profanity"
pattern = '(?i)\b(badword1|badword2)\b'
action = "block"
message = "Message contains prohibited language"
Stage 2: AST Analysis
Structured analysis for code-like content—detects sophisticated injection attempts that regex might miss:
[security.scanning.ast]
enabled = true
languages = ["sql", "javascript", "python", "shell"]
max_parse_size_bytes = 10240
[security.scanning.ast.rules]
# Detect SQL injection via AST parsing
sql_injection = true
# Detect shell command injection
shell_injection = true
# Detect code that accesses the filesystem
filesystem_access = "warn" # "block" | "warn" | "allow"
# Detect network access
network_access = "warn"
pub struct AstScanner {
parsers: HashMap<Language, Box<dyn Parser>>,
rules: AstRules,
}
impl AstScanner {
pub fn scan(&self, content: &str) -> ScanResult {
// Detect the language
if let Some(language) = self.detect_language(content) {
let parser = &self.parsers[&language];
let ast = parser.parse(content);
// Walk the AST looking for dangerous patterns
for node in ast.walk() {
if self.is_dangerous(node, &language) {
return ScanResult::Blocked(ScanReason::AstViolation {
language,
node_type: node.kind().to_string(),
description: self.describe_violation(node),
});
}
}
}
ScanResult::Passed
}
}
Stage 3: Semantic Analysis
AI-powered analysis for subtle threats that bypass pattern matching:
[security.scanning.semantic]
enabled = true
provider = "local" # "local" | "anthropic" | "openai"
model = "classifier-v1" # lightweight classification model
threshold = 0.85 # confidence threshold for blocking
max_latency_ms = 100 # skip if analysis would be too slow
[security.scanning.semantic.categories]
prompt_injection = true
social_engineering = true
data_exfiltration = true
jailbreak_attempt = true
Semantic scanning adds latency. Use a lightweight local model for low-latency scanning, or set max_latency_ms to skip semantic analysis when the system is under load.
CascadeScanner
pub struct CascadeScanner {
regex_scanner: RegexScanner,
ast_scanner: AstScanner,
semantic_scanner: Option<SemanticScanner>,
}
impl CascadeScanner {
pub async fn scan(&self, content: &str) -> ScanResult {
// Stage 1: Regex (fast)
let regex_result = self.regex_scanner.scan(content);
if regex_result.is_blocked() {
return regex_result;
}
// Stage 2: AST (medium)
let ast_result = self.ast_scanner.scan(content);
if ast_result.is_blocked() {
return ast_result;
}
// Stage 3: Semantic (slow, optional)
if let Some(semantic) = &self.semantic_scanner {
let semantic_result = semantic.scan(content).await;
if semantic_result.is_blocked() {
return semantic_result;
}
}
ScanResult::Passed
}
}
CLI Testing
# Test content against scanning rules
echo "SELECT * FROM users WHERE id = 1; DROP TABLE users;" | clawdesk security scan
# Output:
# ❌ BLOCKED by regex scanner
# Rule: sql_injection
# Pattern: DROP TABLE
# Action: block
echo "Hello, how are you?" | clawdesk security scan
# ✅ PASSED all scanning stages
Layer 3: Access Control (ACL)
The ACL system provides fine-grained role-based access control.
Roles and Permissions
[security.acl]
enabled = true
default_role = "user"
# Role definitions
[security.acl.roles.admin]
permissions = ["*"] # all permissions
[security.acl.roles.user]
permissions = [
"message:send",
"message:read",
"tools:web_search",
"tools:calculator",
"tools:knowledge_base",
"session:read",
"session:create",
]
[security.acl.roles.restricted]
permissions = [
"message:send",
"message:read",
]
[security.acl.roles.operator]
permissions = [
"message:*",
"session:*",
"tools:*",
"channels:read",
"config:read",
"cron:read",
]
Permission Format
Permissions follow the pattern resource:action:
| Resource | Actions | Description |
|---|---|---|
message | send, read, delete | Message operations |
session | create, read, delete, compact | Session management |
tools | <tool_name>, * | Tool access |
channels | read, write, admin | Channel management |
config | read, write | Configuration access |
cron | read, write, execute | Cron task management |
plugins | read, install, admin | Plugin management |
security | read, write, admin | Security settings |
* | * | Wildcard (all) |
User Assignments
# Assign roles to users
[security.acl.assignments]
"telegram:12345678" = "admin"
"discord:987654321" = "operator"
"slack:U01234ABCDE" = "user"
"*:*@company.com" = "user" # all company emails get user role
AclManager
pub struct AclManager {
roles: HashMap<String, Role>,
assignments: HashMap<IdentityPattern, String>,
default_role: String,
}
impl AclManager {
/// Check if an identity has a specific permission
pub fn has_permission(&self, identity: &Identity, permission: &str) -> bool {
let role_name = self.get_role(identity);
let role = &self.roles[&role_name];
role.has_permission(permission)
}
/// Get all permissions for an identity
pub fn get_permissions(&self, identity: &Identity) -> Vec<String> {
let role_name = self.get_role(identity);
self.roles[&role_name].permissions.clone()
}
/// Grant a role to an identity
pub fn grant(&mut self, identity: &str, role: &str) -> Result<()>;
/// Revoke a role from an identity
pub fn revoke(&mut self, identity: &str) -> Result<()>;
}
CLI Management
# View ACL rules
clawdesk security acl show
# Grant/revoke roles
clawdesk security acl grant "telegram:12345678" admin
clawdesk security acl revoke "telegram:12345678"
# Check permissions
clawdesk security acl check "telegram:12345678" "tools:code_execution"
# ✅ Allowed: role 'admin' has permission 'tools:code_execution'
Layer 4: Audit Logging
The audit system provides a tamper-evident, hash-chained log of all security-relevant events.
Hash Chain
Each audit entry includes a SHA-256 hash of the previous entry, creating an immutable chain:
AuditLogger
pub struct AuditLogger {
store: Box<dyn AuditStore>,
last_hash: Mutex<[u8; 32]>,
config: AuditConfig,
}
pub struct AuditEntry {
pub id: Uuid,
pub timestamp: DateTime<Utc>,
pub event_type: AuditEventType,
pub identity: Option<Identity>,
pub channel_id: Option<ChannelId>,
pub details: serde_json::Value,
pub prev_hash: [u8; 32],
pub hash: [u8; 32],
}
pub enum AuditEventType {
MessageReceived,
MessageSent,
MessageBlocked,
AuthSuccess,
AuthFailure,
ToolExecuted,
ToolBlocked,
ConfigChanged,
PluginLoaded,
PluginUnloaded,
SessionCreated,
SessionDeleted,
AllowlistModified,
AclModified,
ScanningTriggered,
}
impl AuditLogger {
pub async fn log(&self, event: AuditEventType, details: impl Serialize) -> Result<()> {
let mut last_hash = self.last_hash.lock().await;
let entry = AuditEntry {
id: Uuid::new_v4(),
timestamp: Utc::now(),
event_type: event,
details: serde_json::to_value(&details)?,
prev_hash: *last_hash,
hash: [0; 32], // computed below
..Default::default()
};
// Compute hash: SHA-256(prev_hash || timestamp || event || details)
let hash = compute_hash(&entry);
let entry = AuditEntry { hash, ..entry };
// Store
self.store.append(&entry).await?;
*last_hash = hash;
Ok(())
}
/// Verify the integrity of the audit chain
pub async fn verify(&self) -> Result<VerificationResult> {
let entries = self.store.read_all().await?;
let mut expected_prev = [0u8; 32];
for (i, entry) in entries.iter().enumerate() {
// Verify prev_hash links
if entry.prev_hash != expected_prev {
return Ok(VerificationResult::Broken {
entry_index: i,
expected: hex::encode(expected_prev),
found: hex::encode(entry.prev_hash),
});
}
// Verify self hash
let computed = compute_hash(entry);
if entry.hash != computed {
return Ok(VerificationResult::Tampered {
entry_index: i,
entry_id: entry.id,
});
}
expected_prev = entry.hash;
}
Ok(VerificationResult::Valid {
entries_checked: entries.len(),
})
}
}
Configuration
[security.audit]
enabled = true
storage = "file" # "file" | "sqlite" | "postgres"
path = "${CLAWDESK_DATA_DIR}/audit/audit.log"
rotation = "daily" # "hourly" | "daily" | "size:100MB"
retention_days = 90 # how long to keep audit logs
compress_rotated = true
# Events to audit (all enabled by default)
[security.audit.events]
message_received = true
message_sent = true
message_blocked = true
auth_success = false # disable to reduce volume
auth_failure = true
tool_executed = true
tool_blocked = true
config_changed = true
plugin_events = true
session_events = true
security_changes = true
CLI Operations
# Tail audit log
clawdesk security audit tail --follow
# Example output:
# 2026-02-17T14:23:01Z [MessageReceived] telegram:12345678 → tg_main "Hello"
# 2026-02-17T14:23:01Z [AuthSuccess] telegram:12345678 role=user
# 2026-02-17T14:23:02Z [ToolExecuted] web_search by telegram:12345678
# 2026-02-17T14:23:03Z [MessageSent] → telegram:12345678 via tg_main (1.2s)
# Verify chain integrity
clawdesk security audit verify
# ✅ Audit chain valid: 14,302 entries verified, no tampering detected
# Export audit log
clawdesk security audit export --format json --since "2026-02-01" > audit.json
clawdesk security audit export --format csv --last 1000 > audit.csv
# Search audit log
clawdesk security audit search --event MessageBlocked --since "24h"
DM Pairing
The DmPairingManager handles secure pairing for direct message channels, preventing unauthorized users from initiating conversations:
[security.dm_pairing]
enabled = true
mode = "invite" # "open" | "invite" | "approval"
invite_expiry_hours = 24
max_pending = 100
# Auto-approve patterns
auto_approve = [
"*:*@company.com",
]
| Mode | Behavior |
|---|---|
open | Any user can start a DM conversation |
invite | Users need an invite code to pair |
approval | Admin must approve each new DM pairing |
# Generate an invite code
clawdesk security pairing invite --channel telegram
# Invite code: CLWD-ABC123-XYZ
# Expires: 2026-02-18T14:00:00Z
# Share this code with the user to start a conversation.
# List pending approvals
clawdesk security pairing pending
# Approve/reject
clawdesk security pairing approve telegram:12345678
clawdesk security pairing reject telegram:99999999
Group Policy
The GroupPolicyManager controls agent behavior in group chats:
[security.group_policy]
# How the bot responds in groups
response_mode = "mention" # "all" | "mention" | "reply" | "command"
command_prefix = "/"
mention_name = "@clawdesk"
# Group-specific policies
[security.group_policy.groups."telegram:-100123456789"]
response_mode = "all"
allowed_tools = ["knowledge_base", "calculator"]
max_response_length = 500
[security.group_policy.groups."discord:guild:123456789"]
response_mode = "mention"
allowed_tools = ["web_search"]
rate_limit_per_user = 10 # messages per hour
Scoped Tokens
Create API tokens with limited permissions for integrations:
pub struct ScopedToken {
pub id: TokenId,
pub name: String,
pub scopes: Vec<String>,
pub created_at: DateTime<Utc>,
pub expires_at: Option<DateTime<Utc>>,
pub last_used: Option<DateTime<Utc>>,
pub created_by: Identity,
}
# Create a token
clawdesk security token create \
--name "ci-bot" \
--scope "read:sessions,write:messages" \
--expires "30d"
# Output:
# Token created:
# ID: tok_abc123
# Name: ci-bot
# Scopes: read:sessions, write:messages
# Expires: 2026-03-19T14:00:00Z
# Token: clwd_sk_abcdef123456...
#
# ⚠️ Save this token — it won't be shown again.
# List tokens
clawdesk security token list
# Revoke a token
clawdesk security token revoke tok_abc123
Using Tokens
# API authentication with a scoped token
curl -H "Authorization: Bearer clwd_sk_abcdef123456..." \
http://localhost:1420/api/v1/sessions
Server Secret
The ServerSecret is used for encrypting stored credentials and signing tokens:
[security]
# Auto-generated on first run, stored encrypted
# Override with environment variable for deployments
server_secret = "${CLAWDESK_SERVER_SECRET}"
# Rotate the server secret (re-encrypts all stored credentials)
clawdesk security secret rotate
# ⚠️ This will:
# 1. Generate a new server secret
# 2. Re-encrypt all stored channel tokens
# 3. Invalidate all active API tokens
# 4. Require re-authentication for all providers
# Continue? (y/N)
Rotating the server secret invalidates all existing scoped tokens and re-encrypts stored credentials. Plan this operation during a maintenance window.
Security Best Practices
-
Always use allowlist mode in production —
mode = "allowlist"is the default and most secure option. -
Enable all scanning stages — the cascade scanner is fast thanks to the regex-first approach. Only skip semantic scanning if latency is critical.
-
Use least-privilege roles — assign the most restrictive role that still allows the user to function.
-
Verify audit integrity regularly — run
clawdesk security audit verifyon a schedule. -
Rotate secrets periodically — rotate the server secret and API tokens at least quarterly.
-
Use environment variables for secrets — never store API keys, tokens, or secrets directly in config files.
-
Enable DM pairing — in production, use
inviteorapprovalmode to prevent unauthorized DM interactions. -
Monitor audit logs — set up alerts for
MessageBlocked,AuthFailure, andToolBlockedevents.
Troubleshooting
| Problem | Solution |
|---|---|
| "User not allowed" | Check allowlist with clawdesk security allowlist check <identity> |
| "Permission denied" | Verify role assignment with clawdesk security acl check <identity> <permission> |
| "Message blocked by scanner" | Test content with clawdesk security scan, check regex patterns |
| "Audit verification failed" | Check for filesystem corruption, restore from backup |
| "Token expired" | Create a new token with clawdesk security token create |
| Cannot decrypt credentials | Verify CLAWDESK_SERVER_SECRET is set correctly |