Defence in depth, by default.
openclaw-rs treats security as a runtime property, not a recommendation. Input validation, sandboxing, encrypted credentials, fail-secure errors, and audit logging are wired in before anything else. Five layers, independently load-bearing, deliberately redundant.
What the runtime defends against
Each row pairs a vector with the layer that catches it. Multiple layers catch most vectors — that's the point.
100 KB max message, 50 MB max attachment, 10-attachment cap, JSON depth 32.
bubblewrap (Linux), sandbox-exec (macOS), Job Objects (Windows). Three levels: None / Relaxed / Strict.
ApiKey wraps SecretBox<str>; Debug/Display always print [REDACTED]. Scrubbed in tracing spans.
Encrypted credential store with 0600 file permissions; nonce per record.
validate_path() rejects `..` segments, null bytes, and paths outside the workspace mount.
Default 60 requests/min per client and 30 messages/min per session via tower middleware.
TypeScript plugins run in their own process; communicate over nng with bounded JSON-RPC messages.
#![forbid(unsafe_code)] across every workspace crate.
Layer 1 — Input validation at every boundary
Every external byte hits a validator before anything else looks at it.
- Message size: 100 KB hard cap.
- JSON depth: 32 levels.
- Attachment size: 50 MB per attachment.
- Attachment count: 10 per message.
- Null bytes, control characters, malformed UTF-8 — rejected.
Layer 2 — Capability-based tool registration
Tools must be in the ToolRegistry to be invokable. The LLM can't conjure a tool that wasn't there.
Layer 3 — Platform sandbox
When a registered tool runs, it runs inside an OS-level sandbox.
Three policy levels:
- None: no sandbox; only for trusted internal tools.
- Relaxed: workspace-only writes, network allowed.
- Strict: workspace-only writes, no network, isolated namespace.
Default in production: Strict. Whitelist looser policies per tool.
Layer 4 — Encrypted credential store
Master password → Argon2id KDF (64 MiB / 3 iterations / 4 lanes, 32-byte
output) → AES-256-GCM with a fresh nonce per record. Files at
0600. Tag-authenticated, so any tampering fails the decrypt.
At runtime, secrets live inside ApiKey(SecretBox<str>)
— Debug and Display always print
[REDACTED]; Drop zeroes the buffer.
Layer 5 — Fail-secure errors and audit logging
Every external-facing function returns Result<T, E>.
There is no unwrap() on user input, no panic!()
on parse failure. Errors return sanitised messages; the underlying cause
is logged with full context internally via tracing.
Audited events:
- Authentication attempts (success and failure).
- Authorisation decisions.
- Tool executions, with parameters.
- Configuration changes.
- Rate-limit triggers.
Rate limiting
Tower middleware enforces 60 requests/min per client at the gateway and
30 messages/min per session at the agent layer. Override in
openclaw.json if your workload requires it.
Dependency hygiene
cargo-deny is part of CI. We reject crates with known
security advisories, copyleft licenses (we ship MIT), and unmaintained
flags without a reason. Preferred: rustls over
openssl; aes-gcm + argon2 from
RustCrypto; secrecy for sensitive types.
What you still own
The runtime is not your tool code. If you register a tool that does
format!("rm [object Object]", user_input), no sandbox saves you from
yourself — the command was authorised by your registered code.
Validate inside your tools too. Use serde to parse params
into typed structures. Use validator or garde to
constrain fields. Never unwrap() on a model-supplied value.
Responsible disclosure
Security issues should not be filed as public GitHub issues. See
docs/SECURITY.md in the repo for the disclosure address and
PGP key.