Skip to content

Security model

Revka's defence-in-depth security layers from policy engine to OS sandboxing, encryption, and audit.

Revka is audit-first by design. Before any agent tool runs, the action passes through independent, overlapping security layers — a policy engine, OS-level sandboxing, encrypted credential storage, and a tamper-evident audit log. This page explains those layers, how they compose, the philosophy behind them, and how to report a vulnerability.

Read this to build a mental model of where enforcement happens and which knobs to turn for your threat profile. For copy-paste configuration and command reference, follow the links into the dedicated Security section as you go.

Every defence is evaluated before a tool is allowed to act, and no single bypass defeats the model — a command must clear the autonomy gate, the allowlist, risk classification, path sandboxing, and rate limits, then run inside an OS sandbox, with the whole turn recorded to an append-only audit log.

Policy engine

SecurityPolicy enforces an autonomy level, a command allowlist with a quote-aware shell parser, risk classification, a workspace boundary, and a sliding-window rate limiter.

OS sandboxing

Optional kernel/user-space isolation backends (Firejail, Bubblewrap, Landlock, sandbox-exec, Docker) wrap command execution, auto-detected at startup.

Encryption & auth

Secrets are encrypted at rest with ChaCha20-Poly1305; pairing tokens are SHA-256 hashed; bearer tokens gate the gateway API.

Audit & containment

A tamper-evident Merkle audit log records every event; emergency stop, OTP gating, and trust scoring contain a misbehaving agent in real time.

The model protects against the agent’s three highest-risk failure modes: unauthorized shell execution (rm -rf /, curl | sh), workspace escape (path traversal, symlinks, absolute paths), and runaway cost from LLM API calls.

The layers below are ordered from the innermost policy check outward. Each is independent: a tool call must satisfy all of them.

The policy engine’s entry point is a three-tier autonomy mode that controls whether the agent can act at all. The current level is injected into the LLM system prompt automatically.

LevelBehaviour
read_onlyObserve only — all tool actions are blocked, even safe reads like ls/cat. Safest choice for untrusted channels.
supervisedDefault. Acts within allowlists; medium- and high-risk commands require an explicit approval (approved = true).
fullNo approval gates, but still enforces the command allowlist and workspace boundary.
[autonomy]
level = "supervised" # read_only | supervised | full

Every shell command is checked against an explicit allowlist before execution. A quote-aware lexer splits compound input on unquoted ;, |, &&, ||, and newlines, then validates each sub-command independently — so chaining cannot smuggle a blocked command past the gate.

[autonomy]
allowed_commands = ["git", "npm", "cargo", "ls", "cat", "grep", "find",
"echo", "pwd", "wc", "head", "tail", "python", "node"]

Five independent gates run, in order:

  1. Subshell operators — backticks, $(...), and process substitution <(...) / >(...) are rejected.
  2. Redirections< and > are rejected.
  3. tee — always blocked regardless of the allowlist (it writes to arbitrary files).
  4. Background & — a single trailing & is rejected.
  5. Per-segment allowlist — each split sub-command must match by basename, full path, or the wildcard "*".

Allowlist entries may be bare names (matched by basename), full paths (e.g. /usr/bin/antigravity, exact match only), or "*". Even with "*", dangerous argument patterns stay blocked: find -exec, find -ok, git config, git -c, and git alias.

Each command — and each segment of a compound command — is classified Low / Medium / High risk; the highest classification across all segments wins.

RiskExamplesDefault handling
Highrm, dd, mkfs, sudo, su, chmod, chown, mount, iptables, curl, wget, nc, ssh, scp; Windows del, format, reg, net, runas, powershellBlocked unless explicitly allowlisted
Mediumgit state-changers (commit, push, reset, rebase, checkout); npm/cargo install/publish; mkdir, mv, cp, lnRequires approval in supervised
Lowls, cat, grep, pwd, echo, dateAllowed (subject to allowlist)
[autonomy]
block_high_risk_commands = true # default
require_approval_for_medium_risk = true # default

Six independent checks keep file operations inside the workspace, plus a post-canonicalization check that defeats symlink escapes:

  1. Null-byte injection rejection.
  2. .. component traversal rejection.
  3. URL-encoded traversal (..%2f) rejection.
  4. ~user tilde-expansion rejection.
  5. Workspace-only absolute-path lock — absolute paths must fall under workspace_dir or allowed_roots.
  6. Forbidden-prefix blocklist (/etc, /root, ~/.ssh, ~/.aws, …).
[autonomy]
workspace_only = true # default
allowed_roots = ["/data/shared", "~/projects/other"]
forbidden_paths = ["/etc", "/root", "~/.ssh", "~/.aws", "~/.config"]

Workspace and allowed_roots are checked before forbidden_paths, so a workspace nested inside an otherwise-forbidden tree (for example a workspace under /home when /home is forbidden) is still allowed. Revka’s own runtime config files (config.toml, active_workspace.toml) are separately guarded against agent modification — silently and non-overridably.

A sliding one-hour window caps how many Act-type operations the agent can run; a separate daily cap bounds LLM spend. Read operations are never rate-limited.

KeyTypeDefaultMeaning
max_actions_per_houru3220In-memory sliding window; resets on daemon restart
max_cost_per_day_centsu32500Daily LLM spend cap (US$5.00)
[autonomy]
max_actions_per_hour = 20
max_cost_per_day_cents = 500

On top of the application-layer policy, command execution can be wrapped in an OS isolation backend. Auto-detection picks the best available backend at startup; if a requested backend is unavailable, Revka falls back to a no-op sandbox with a warning.

BackendPlatformNotes
firejailLinuxUser-space, no root. Easiest to set up.
bubblewrapLinux/macOSUser-namespace containers (feature sandbox-bubblewrap).
landlockLinux 5.13+Kernel-enforced filesystem restrictions; strongest isolation (feature sandbox-landlock).
sandbox-execmacOSApple Seatbelt profiles.
dockerAllContainer isolation; adds spawn overhead.
noneAllApplication-layer only; always-available fallback.
[security.sandbox]
enabled = true
backend = "auto" # auto | none | firejail | bubblewrap | landlock | sandbox-exec | docker

Every security-relevant event is appended to a JSONL audit log. Each entry carries a SHA-256 entry_hash computed over the previous hash plus the canonical JSON of the entry, a monotonic sequence, and a prev_hash back-link — forming a Merkle chain. Editing any entry invalidates every subsequent hash. Entries can optionally be signed with HMAC-SHA256.

[audit]
enabled = true # default
log_path = "audit.log" # relative to the revka dir
max_size_mb = 100
sign_events = false # optional HMAC signing

HMAC signing requires a 32-byte key, set before the gateway starts:

Terminal window
export REVKA_AUDIT_SIGNING_KEY="<64 hex chars = 32 bytes>"

Query and verify the chain over the gateway API:

Terminal window
# Query recent events
curl "http://127.0.0.1:42617/api/audit?limit=50&event_type=command_execution" \
-H "Authorization: Bearer rk_<token>"
# Verify chain integrity (detects tampering)
curl "http://127.0.0.1:42617/api/audit/verify" \
-H "Authorization: Bearer rk_<token>"

GET /api/audit accepts limit (default 50, max 500), event_type (command_execution, file_access, config_change, auth_success, auth_failure, policy_violation, security_event), and since (RFC 3339). On rotation the chain resets to genesis, so each rotated archive verifies independently.

Two cryptographic layers protect credentials and gateway access:

  • Encrypted secret store. API keys and tokens written to config are encrypted with ChaCha20-Poly1305 AEAD before storage, keyed by a 32-byte random key at ~/.revka/.secret_key (mode 0600 on Unix; restricted ACL on Windows). Encrypted values appear as enc2:<hex>. Back up the key file — losing it makes every enc2: value unrecoverable.
  • Device pairing & bearer tokens. With require_pairing = true (default), first startup prints a 6-digit one-time code. A client exchanges it via POST /pair for a 256-bit bearer token prefixed rk_. Tokens are stored only as SHA-256 hashes; after 5 failed attempts a client is locked out for 5 minutes.
[secrets]
encrypt = true # default
[gateway]
require_pairing = true # default
allow_public_bind = false # must be explicit to bind beyond localhost
Terminal window
# Server prints "Pairing code: 123456" on first start
curl -X POST http://127.0.0.1:42617/pair \
-H 'Content-Type: application/json' \
-d '{"code": "123456", "device_name": "My Laptop", "device_type": "cli"}'
# Returns: {"token": "rk_<64 hex chars>"}

Three subsystems let you contain a misbehaving agent without restarting it:

  • Emergency stop (revka estop). Persisted kill switch with four levels — kill-all, network-kill, domain-block (glob patterns), and tool-freeze. State survives restarts and is fail-closed: corrupt or unreadable state defaults to kill_all = true. Resume can require a valid OTP.
  • OTP gating. TOTP (RFC 6238) codes gate specific tools, actions, or domains. The enrollment otpauth:// URI is shown once at first use.
  • Trust scoring. Each domain carries a 0.0–1.0 trust score that decays toward its initial value, drops on corrections, and rises on success. Below the regression threshold (default 0.5), autonomy is automatically downgraded one tier (full → supervised → read_only).
Terminal window
revka estop # engage kill-all
revka estop --level domain-block --domain "*.chase.com"
revka estop --level tool-freeze --tool shell
revka estop resume --otp 123456 # OTP-gated resume
[security.otp]
enabled = true
gated_actions = ["shell", "file_write", "browser_open"]
gated_domains = ["*.chase.com"]
[security.estop]
enabled = true
require_otp_to_resume = true
[trust]
initial_score = 0.8
regression_threshold = 0.5

Two heuristic scanners protect the boundary between the agent and the outside world:

  • Prompt injection defence (PromptGuard). Scans inbound messages for six categories — system-prompt override, role confusion, tool-call JSON injection, secret extraction, command-injection metacharacters, and jailbreak patterns — returning Safe, Suspicious, or Blocked. Configurable action and sensitivity.
  • Outbound leak detection (LeakDetector). Scans outbound content before channel delivery for provider API keys, AWS credentials, PEM private keys, JWTs, database URLs, and high-entropy tokens, replacing matches with [REDACTED_*] placeholders.
[security.prompt_guard]
action = "warn" # warn | block | sanitize
sensitivity = 0.7 # 0.0 lenient — 1.0 strict
[security.leak_detector]
sensitivity = 0.7

Two design principles shape how these layers are delivered.

Security never costs you portability. The heavyweight protections are optional Cargo feature flags behind platform-specific conditional compilation, so when a feature is disabled its code is not compiled in — zero binary bloat. Sandbox backends are pluggable behind a single Sandbox trait (just like providers, channels, and memory), and the same binary runs across Linux (x86_64, ARM, RISC-V), macOS, and Windows, adapting its protection level to what the host supports.

Terminal window
# Standard release — application-layer security always on
cargo build --release
# Add a platform sandbox at compile time
cargo build --release --features sandbox-landlock # Linux
cargo build --release --features sandbox-bubblewrap # macOS/Linux

Security should be like an airbag — present, protective, and invisible until needed. The onboarding wizard adds no extra steps and writes no security sections to config.toml unless you customize them; sensible defaults (supervised autonomy, workspace-scoped access, secrets encryption, auto-detected sandbox) apply silently. Advanced users keep explicit control through the [autonomy], [security.*], and [secrets] config sections.

The layers documented above are enforced by the runtime today. A few related capabilities are gated or staged:

CapabilityStatus
Policy engine, command allowlist, risk classification, path sandboxing, rate limitingCurrent
Audit log, encrypted secrets, device pairing, OTP, emergency stop, prompt-injection & leak detection, trust scoringCurrent
OS sandbox backends (Firejail, Bubblewrap, Landlock, sandbox-exec, Docker)Current, behind feature flags / auto-detection
WebAuthn / FIDO2 gateway loginFeature-gated — build with --features webauthn
Nevis IAM (OAuth2/OIDC, FIDO2, role mappings)Enterprise — see Enterprise IAM (Nevis)
Verifiable Intent (SD-JWT commerce gating)Advanced — see Verifiable Intent

Revka’s Docker images follow CIS Docker Benchmark practices: the runtime stage is a distroless nonroot image with no shell and no package manager, the container runs as a non-root UID, and a read-only root filesystem is supported.

ControlImplementation
Non-root userRuns as UID 65534 (distroless nonroot)
Minimal base imagegcr.io/distroless/cc-debian12:nonroot
Read-only filesystemdocker run --read-only with a /workspace volume
Terminal window
# Verify the image runs as non-root
docker build -t revka .
docker inspect --format='{{.Config.User}}' revka
# Expected: 65534:65534
# Run hardened: read-only root, writable workspace volume
docker run --read-only -v /path/to/workspace:/workspace revka gateway

CI enforces that the container does not run as root, that the runtime stage uses the :nonroot variant, and that an explicit numeric USER directive exists. For deployment guidance, see Docker, Compose & one-click PaaS.

Please report security vulnerabilities responsibly — do not open a public GitHub issue.

  1. Open a private advisory via GitHub Security Advisories, or use GitHub private vulnerability reporting to reach the maintainers.

  2. Include a description, reproduction steps, an impact assessment, and a suggested fix if you have one.

  3. Expect an acknowledgment within 48 hours, an assessment within one week, and a fix within two weeks for critical issues.