Please do NOT open a public GitHub issue for security vulnerabilities.
Contact: development@securtel.net Expected response: within 72 hours Coordinated disclosure: 90 days before public disclosure requested
AlexClaw includes built-in session-based authentication. The web interface
is fully protected — all routes except /login require an authenticated session.
There is no anonymous access to any admin functionality.
Authentication is configured via the ADMIN_PASSWORD environment variable
(see .env.example). ADMIN_PASSWORD is always required — if it is not set,
the login page will show an error and no access is granted.
TOTP-based 2FA protects all sensitive operations. Setup via /setup 2fa
from Telegram or Discord — compatible with any TOTP authenticator
(Google Authenticator, Authy, etc.).
Operations requiring 2FA (mandatory, no bypass):
- Skill load — uploading and compiling a new dynamic skill (Admin UI only)
- Skill unload — removing a dynamic skill from the registry (Admin UI only)
- Skill reload — recompiling an existing dynamic skill (Admin UI only)
- Shell commands —
/shellvia Telegram/Discord - Workflows marked
Requires 2FA— configurable per workflow
Cross-channel verification: When a skill operation is triggered from the Admin UI, the 2FA challenge is sent to ALL active gateways (Telegram and Discord). The user can respond with their 6-digit code from either channel. This enables phone-based verification for web UI actions.
Skill management is Admin UI only. The /skill load|unload|reload
commands are not available from Telegram/Discord — you cannot upload code
from a messaging app. The /skills command still lists registered skills,
and skills execute normally within workflows.
AlexClaw only responds to messages from the configured TELEGRAM_CHAT_ID.
Messages from any other chat ID are silently ignored.
Do not share your bot token — anyone with the token can send commands
if they know or guess your chat ID.
The /webhooks/github endpoint verifies all incoming payloads using
HMAC-SHA256 with Plug.Crypto.secure_compare for timing-safe comparison.
Webhooks without a valid signature are rejected with 401.
If no webhook secret is configured, all webhooks are rejected.
Set github.webhook_secret in Admin > Config (GitHub category).
The MCP endpoint (/mcp) exposes AlexClaw skills, workflows, and data to external AI clients (Claude Code, Cursor, Claude Desktop) via the Model Context Protocol.
Bearer token auth: Every MCP request must include Authorization: Bearer <token>. The token is validated against mcp.api_key stored in Admin > Config using Plug.Crypto.secure_compare/2 (constant-time comparison, no timing attacks). Requests without a valid token receive 401.
Token management:
- Store the API key encrypted in PostgreSQL (
sensitive: trueon the config setting) - No automatic expiration — treat MCP tokens like long-lived API keys
- Rotate by updating
mcp.api_keyin Admin > Config — all previous tokens are immediately invalidated - If compromised: rotate immediately in Admin > Config
Policy enforcement: MCP tool calls pass through PolicyEngine.evaluate/2 with :mcp caller type. The mcp_restriction policy rule type blocks tools by name pattern — insert a policy with rule_type: "mcp_restriction" and config: {"tool_pattern": "shell", "action": "deny"} to block any tool matching that pattern.
Resource filtering: Sensitive config values (API keys, OAuth tokens) are redacted in MCP resource responses — only [REDACTED] is returned for settings marked sensitive: true.
Audit logging: All MCP tool invocations are logged to auth_audit_log with caller mcp:<tool_name>, visible in Admin > Policies > Audit Log.
Hardening recommendations:
- The
/mcpendpoint must be behind TLS — never transmit Bearer tokens over plain HTTP - Store the MCP API key in your client's config securely (environment variable or encrypted config)
- Monitor the Audit Log for unexpected MCP activity
- Use
mcp_restrictionpolicies to limit which tools are available to MCP clients - If MCP is not needed, do not set
mcp.api_key— the auth plug rejects all requests when the key is unconfigured
Multi-node clusters authenticate via BEAM's distributed Erlang protocol:
- All nodes must share the same
CLUSTER_COOKIE(set via environment variable) - EPMD (Erlang Port Mapper Daemon) on port 4369 coordinates node discovery
- Nodes without the correct cookie cannot join the cluster or trigger remote workflows
- The
receive_from_workflowgate skill provides an additional per-workflow access control layer via optionalallowed_nodesconfig
Hardening recommendations:
- Generate
CLUSTER_COOKIEwithopenssl rand -base64 32— treat it likeSECRET_KEY_BASE - EPMD port (4369) and BEAM distribution ports (dynamic, high range) should NOT be exposed to the internet
- Restrict inter-node traffic to private networks, VPCs, or Docker networks
- When running across machines, use VPN or SSH tunnels between Docker hosts
The /shell command allows the owner to run OS commands inside the container
for diagnostics (disk, memory, connectivity, BEAM status). It is protected
by 5 layers of defense-in-depth:
- Disabled by default —
shell.enabledmust be explicitly set totruein Admin > Config. The check is enforced both in the Dispatcher and inside the skill itself. - 2FA gate — every
/shellcommand requires TOTP verification when 2FA is enabled. - Whitelist with word-boundary check — the command must start with an allowed prefix (
df,free,ps,uptime,git, etc.). The prefix is boundary-checked:"df"allows"df -h"but not"define". The whitelist is stored as a JSON array in the database and editable from Admin > Config. - Blocklist — commands containing shell metacharacters (
&&,||,|,;,`,$(,>,<,\n) are rejected even if the prefix is whitelisted. - No shell interpretation — commands are executed via
System.cmd/3with arguments passed as a list (parsed byOptionParser.split/1). No shell is invoked — no globbing, no piping, no variable expansion.
Additional protections:
- Timeout — commands are killed after 30 seconds (configurable via
shell.timeout_seconds) - Output truncation — output is capped at 4000 characters (configurable via
shell.max_output_chars) - Workflow mode — when used in workflows, the command comes from step config (not user input), preventing injection through workflow chaining
The web-automator sidecar runs a real browser with network access. Automation recipes execute arbitrary browser actions — review recorded recipes before assigning them to scheduled workflows. The noVNC interface (port 6080) should never be exposed publicly.
The db_backup core skill produces gzip-compressed pg_dump files on a
host-mounted directory. Backups contain the full database contents
including encrypted API keys and tokens (stored as AES-256-GCM ciphertext).
Security considerations:
- Backup files should be stored on an encrypted filesystem or encrypted at
the host level — the
pg_dumpoutput contains encrypted values but also plaintext data (workflows, memories, knowledge entries, settings metadata) - Restrict host directory permissions (
chmod 700) to prevent unauthorized access - The skill verifies the backup directory is a real bind mount, not the container overlay FS — this prevents backups from being silently lost on container recreation
- Backup rotation (configurable
backup.max_files) limits exposure window — old backups are deleted automatically - To restore:
gunzip -c backup.sql.gz | psql -U alexclaw -d alex_claw_prodfrom a host with access to the database
Sensitive configuration values (API keys, tokens, OAuth secrets) are encrypted at the application level using AES-256-GCM before being stored in PostgreSQL.
- Encryption key is derived from
SECRET_KEY_BASEvia HKDF-SHA256 - Each value gets a unique 12-byte random IV — identical plaintext produces different ciphertext
- Encrypted values are stored with an
enc:prefix (base64-encoded IV + ciphertext + GCM tag) - Decryption happens transparently on boot (ETS cache holds plaintext for runtime use)
- The admin UI displays masked values — never raw ciphertext or full plaintext
Sensitive keys (automatically marked and encrypted):
telegram.bot_token, llm.gemini_api_key, llm.anthropic_api_key,
github.token, github.webhook_secret, google.oauth.client_secret,
google.oauth.refresh_token
Important: If you change SECRET_KEY_BASE, all encrypted settings become
unreadable. You will need to re-enter API keys and tokens via the admin UI
or environment variables and restart.
Dynamic skills are compiled into the BEAM VM at runtime via Code.compile_file.
The following protections are in place:
- 2FA mandatory — every load, unload, and reload requires TOTP verification via Telegram/Discord. No exceptions, no config toggle
- Admin UI only — skill management is not available from Telegram/Discord commands. Code cannot be uploaded from a messaging app
- Version bump enforcement — loading a skill that's already loaded with the same version is rejected. The developer must bump
version/0or use reload to force - Path restriction — only files inside the configured
SKILLS_DIRvolume are accepted - Namespace enforcement — module must be
AlexClaw.Skills.Dynamic.* - Behaviour validation — module must export
run/1 - Permission sandbox — skills declare permissions;
SkillAPIenforces them at runtime. Undeclared permissions return{:error, :permission_denied} - Integrity checksums — SHA256 of source file stored on load, verified on boot. Mismatched files are skipped
- Core protection — core skills cannot be unloaded or overwritten by dynamic skills
- No NIF compilation — the Alpine runtime image has no build tools, preventing native code loading
- External skill detection (AST-based) — at load time, dynamic skill source is AST-scanned for calls to HTTP/socket libraries (Req, HTTPoison, Finch, Tesla,
:gen_tcp,:httpc, SkillAPI.http_*). If detected withoutdef external, do: true, the skill is rejected (fail-closed). This prevents untagged dynamic skills from fetching external data without proper sanitization. Note: this scan is single-module only — indirect calls through helper modules are not caught in v1
Circuit breaker protection: Each skill (core and dynamic) is wrapped by an OTP circuit breaker. After 3 consecutive failures, the circuit opens and calls are rejected instantly without executing the skill. This prevents a failing dynamic skill from consuming resources or cascading failures through workflows. Workflow steps can be configured to skip or fallback to an alternative skill when a circuit is open or a skill is missing.
Autonomous Skill Generation (Coder Skill):
The /coder command uses a local LLM to generate dynamic skills from natural
language descriptions. Generated code passes through the same validation pipeline
as manually-loaded skills (namespace, behaviour, permission checks). Additional
safety measures:
- Filename validation rejects path traversal (
..,/,\) - Only
.exfiles can be written - Writes are confined to the configured skills directory
- Generated workflows are created in disabled state
- All generated code is logged via
Logger.infofor audit - Always uses
tier: :local— zero cloud API cost - Retry bound prevents infinite loops (configurable, default 3)
What is NOT sandboxed: A dynamic skill runs in the same BEAM VM as the rest of AlexClaw. A malicious skill could bypass SkillAPI by calling internal modules directly. The permission system is a guardrail, not a security boundary. Only load skills from sources you trust.
External-facing skills (web_search, web_browse, api_request, rss_collector,
github_security_review, google_calendar, google_tasks, web_automation,
research) fetch data from untrusted sources. This data flows through the
workflow engine and may reach the LLM, creating a prompt injection surface.
External skill tagging: Skills that fetch external data declare
def external, do: true (callback on AlexClaw.Skill behaviour). The
SkillRegistry tracks this flag in ETS and exposes external?/1 for
runtime checks.
7-layer heuristic sanitizer (AlexClaw.ContentSanitizer):
Content from external skills passes through 7 defense layers before reaching the LLM:
- Hidden HTML detection — detects and logs content in
<noscript>,<template>,aria-hidden="true"elements before stripping - Hidden CSS detection — detects and logs content with
display:none,visibility:hidden,font-size:0,color:transparent,opacity:0, off-screen positioning (left:-9999px),text-indent:-9999px,clip:rect(0...) - Zero-width unicode stripping — removes 19 types of invisible characters
used for steganographic injection (
U+200BthroughU+180E,U+FEFF, etc.) - HTML stripping — Floki-based extraction of semantic text only (script, style, noscript, template, meta, head, svg removed)
- Size guard — configurable max content size (default 10KB), truncates oversized payloads
- Pattern matching — 101 known injection phrases loaded from
config/injection_patterns.jsonat runtime (updatable without recompilation). Patterns sourced from NVIDIA Garak probe library covering DAN, developer mode, instruction override, persona hijacking, token penalty, encoding tricks, and more - Imperative tone heuristic — detects directive language (second-person pronouns + imperative verbs like "ignore", "forget", "obey", "execute", "reveal") to catch novel payloads not in the pattern list
Pre-LLM sanitization: web_browse and web_search sanitize fetched
content before building the LLM prompt. Injection payloads are stripped
before the model ever sees them.
Post-LLM sanitization: The workflow executor auto-sanitizes output from
any skill tagged external?/1 == true, catching skill name leaks or
residual injection artifacts in the LLM response.
Stripped sentences are logged with their detection reason ([pattern],
[imperative], [skill_mention]) for forensic analysis.
Known limitations:
- Pattern matching cannot catch novel injection techniques not in the JSON file
- The imperative tone heuristic may produce false positives on legitimate directive text (e.g., "Experts recommend..." is preserved, but edge cases exist)
- Encoding-based attacks (Base64, ROT13) bypass pattern matching — the encoded payload reaches the LLM, though most models don't decode and follow them
- Future: embedded tiny classifier model (Qwen2.5-0.5B / SmolLM2-360M) for binary injection classification as a second pass on ambiguous sentences
AlexClaw implements a composable authorization layer for skill execution, inspired by Macaroon-style capability tokens and policy-as-code evaluation.
Context-aware permission checks:
Every SkillAPI call builds an AuthContext (caller, type, permission,
chain depth, workflow run ID, timestamp) and evaluates it through the
PolicyEngine. Core skills bypass all checks (trusted code). Dynamic
skills are checked against their declared permissions, capability tokens,
and active policy rules.
Capability tokens (Macaroon-style):
When a workflow executes, each step receives an HMAC-signed capability
token scoped to the skill's declared permissions. Cross-skill invocation
via run_skill/3 attenuates the token — a child skill can only receive
a subset of the caller's permissions, never more. Tokens are signed
with a key derived from SECRET_KEY_BASE via HKDF-SHA256.
Chain depth enforcement: Skill-invokes-skill chains are limited to depth 3 (configurable). Prevents infinite recursion and limits blast radius of cross-skill calls.
Process isolation for dynamic skills:
Dynamic skills run in a separate spawned process (SafeExecutor).
The capability token is set in the child's process dictionary, isolating
it from the caller. Core skills run in-process (no overhead).
Policy rules (configurable via Admin > Policies):
All policy configs are JSON objects. The permission field is optional —
omit it to apply the rule to all permissions. Higher priority rules
are evaluated first.
rate_limit — max N calls per time window per skill/permission.
{"permission": "llm", "max_calls": 20, "window_seconds": 60}Blocks the skill after 20 LLM calls within 60 seconds. Omit permission
to limit all SkillAPI calls globally. Counters are per-skill, in-memory
(reset on restart).
time_window — deny a permission during specific UTC hours.
{"permission": "web_read", "deny_start_hour": 0, "deny_end_hour": 6}Blocks web_read between 00:00 and 06:00 UTC. Useful to prevent
scheduled workflows from hitting external APIs during maintenance windows.
chain_restriction — prevent a skill from invoking other skills.
{"caller_pattern": "Coder"}Any skill whose module name contains "Coder" will be denied when it
tries to invoke another skill via run_skill/3 (chain_depth > 0).
The pattern is a substring match on the full module name.
permission_override — temporarily deny (or allow) a specific permission.
{"permission": "memory_write", "action": "deny", "expires_at": "2026-04-01T00:00:00Z"}Denies memory_write for all dynamic skills until the expiry date.
Omit expires_at for a permanent override. Set action to "deny"
to block — any other value (or omitting it) has no effect.
Policies are stored in PostgreSQL, cached in ETS (30s TTL), and manageable from Admin > Policies. Changes take effect within 30 seconds.
Audit logging:
All authorization denials are persisted to the auth_audit_log table
with full context (caller, permission, reason, chain depth, workflow run).
Viewable from Admin > Policies > Audit Log. Auto-pruned after 30 days.
Limitation: A malicious dynamic skill can still bypass SkillAPI by calling internal modules directly. The authorization layer is enforcement at the API boundary, not a sandbox. Only load skills from trusted sources.
LLM prompts may contain user data. Workflow steps send data to external LLM providers (Anthropic, Google Gemini). Review which providers are enabled and their data retention policies before processing sensitive information.
Built-in login rate limiting. Failed login attempts are tracked per IP using ETS. After 5 failures (configurable), the IP is blocked for 15 minutes (configurable). Limits are adjustable at runtime from the Config UI without restart.
- Run behind a reverse proxy with TLS — never expose port 5001 directly
- Set a strong random
SECRET_KEY_BASE(mix phx.gen.secret) — this is also the encryption key material for sensitive config values - Set
ADMIN_PASSWORDto a strong random value - Restrict PostgreSQL to localhost or internal network only
- Built-in login rate limiting is active by default (configurable via Config UI)
- Never expose noVNC port (6080) publicly — it provides unauthenticated browser access
AlexClaw is designed as a single-user personal agent. Multi-user access control is not in scope. The authentication model assumes a single trusted operator.