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 is available for sensitive Telegram commands.
Setup via /setup 2fa — compatible with any TOTP authenticator
(Google Authenticator, Authy, etc.).
Workflows can be marked Requires 2FA in the admin UI,
requiring code verification before execution.
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).
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.
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:
- 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
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.
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.