Skip to main content

Overview

OpenFang doesn’t bolt security on after the fact. Every layer is independently testable and operates without a single point of failure. 16 discrete security systems organized into critical fixes and state-of-the-art defenses:

The 16 Security Systems

Tool code runs in WebAssembly with two independent safety mechanisms:

Fuel Metering

Every WASM instruction consumes “fuel”. When fuel runs out, execution stops.
let mut store = wasmtime::Store::new(&engine, ());
store.set_fuel(1_000_000)?;  // 1M instruction budget

// After execution:
let remaining = store.get_fuel()?;
if remaining == 0 {
    return Err("Fuel exhausted");
}

Epoch Interruption

A watchdog thread increments an epoch counter every second. WASM execution checks the epoch and terminates if too much wall-clock time passes.
engine.increment_epoch();  // Called by watchdog thread

// In WASM config:
config.epoch_interruption(true);
store.set_epoch_deadline(10);  // 10-second timeout
This prevents:
  • CPU-bound runaway code (caught by fuel metering)
  • Time-bound hangs (caught by epoch interruption)
Code reference: crates/openfang-runtime/src/sandbox.rs
Every agent action is cryptographically linked to the previous one using a Merkle hash chain.

How It Works

pub struct AuditEntry {
    pub timestamp: DateTime<Utc>,
    pub agent_id: AgentId,
    pub action: String,
    pub details: serde_json::Value,
    pub prev_hash: String,  // SHA256 of previous entry
    pub hash: String,       // SHA256 of this entry
}

impl AuditEntry {
    pub fn compute_hash(&self) -> String {
        let data = format!(
            "{}{}{}{}{}",
            self.timestamp.to_rfc3339(),
            self.agent_id,
            self.action,
            serde_json::to_string(&self.details).unwrap(),
            self.prev_hash
        );
        hex::encode(sha2::Sha256::digest(data.as_bytes()))
    }
}

Tamper Detection

If someone modifies any entry in the audit log, the hash chain breaks:
pub fn verify_audit_chain(entries: &[AuditEntry]) -> Result<(), String> {
    for i in 1..entries.len() {
        let prev = &entries[i - 1];
        let curr = &entries[i];

        // Check that curr.prev_hash matches prev.hash
        if curr.prev_hash != prev.hash {
            return Err(format!("Chain broken at entry {}", i));
        }

        // Check that curr.hash is valid
        if curr.hash != curr.compute_hash() {
            return Err(format!("Invalid hash at entry {}", i));
        }
    }
    Ok(())
}
Tamper with one entry → entire chain breaks.Code reference: crates/openfang-runtime/src/audit.rs
Labels propagate through execution — secrets are tracked from source to sink.

Taint Labels

pub enum TaintLabel {
    Secret,           // API keys, passwords
    PII,              // Personal identifiable information
    ExternalInput,    // User input, web scraping
    SystemCommand,    // Shell commands
    NetworkData,      // Data from network requests
}

pub struct TaintSet {
    labels: HashSet<TaintLabel>,
}

impl TaintSet {
    pub fn union(&self, other: &TaintSet) -> TaintSet {
        TaintSet {
            labels: self.labels.union(&other.labels).cloned().collect(),
        }
    }

    pub fn has(&self, label: &TaintLabel) -> bool {
        self.labels.contains(label)
    }
}

Example: Secret Leakage Prevention

let api_key = load_api_key();  // Tainted with TaintLabel::Secret
let response = llm_call(&api_key)?;  // Response inherits Secret taint

// Later:
if response.taint_set.has(&TaintLabel::Secret) {
    // Block: Cannot log secret-tainted data
    return Err("Cannot log secret-tainted data");
}
Code reference: crates/openfang-types/src/taint.rs
Every agent identity and capability set is cryptographically signed using Ed25519.

Signing

use ed25519_dalek::{Keypair, Signature, Signer};

let keypair = Keypair::generate(&mut rand::thread_rng());
let manifest_bytes = serde_json::to_vec(&manifest)?;
let signature: Signature = keypair.sign(&manifest_bytes);

let signed_manifest = SignedManifest {
    manifest,
    signature: signature.to_bytes().to_vec(),
    public_key: keypair.public.to_bytes().to_vec(),
};

Verification

use ed25519_dalek::{PublicKey, Signature, Verifier};

let public_key = PublicKey::from_bytes(&signed_manifest.public_key)?;
let signature = Signature::from_bytes(&signed_manifest.signature)?;
let manifest_bytes = serde_json::to_vec(&signed_manifest.manifest)?;

public_key.verify(&manifest_bytes, &signature)?;
// If this passes, the manifest is authentic
Prevents:
  • Manifest tampering (signature breaks if modified)
  • Impersonation (only holder of private key can sign)
Code reference: crates/openfang-types/src/manifest_signing.rs
Blocks requests to private IPs and cloud metadata endpoints.

Blocked Targets

const SSRF_BLOCKED: &[&str] = &[
    "169.254.169.254",  // AWS metadata
    "metadata.google.internal",  // GCP metadata
    "127.0.0.1",
    "localhost",
    "0.0.0.0",
];

fn is_private_ip(ip: &std::net::IpAddr) -> bool {
    match ip {
        std::net::IpAddr::V4(ipv4) => {
            ipv4.is_private()
                || ipv4.is_loopback()
                || ipv4.is_link_local()
                || ipv4.is_broadcast()
        }
        std::net::IpAddr::V6(ipv6) => {
            ipv6.is_loopback() || ipv6.is_unspecified()
        }
    }
}

pub fn is_ssrf_target(url: &str) -> bool {
    if let Ok(parsed) = url::Url::parse(url) {
        if let Some(host) = parsed.host_str() {
            // Check against blocklist
            if SSRF_BLOCKED.contains(&host) {
                return true;
            }

            // Resolve DNS and check if private IP
            if let Ok(addrs) = (host, 0).to_socket_addrs() {
                for addr in addrs {
                    if is_private_ip(&addr.ip()) {
                        return true;
                    }
                }
            }
        }
    }
    false
}
Applied in:
  • web_fetch tool
  • host_net_fetch WASM host function
  • All HTTP client calls
Code reference: crates/openfang-runtime/src/web_fetch.rs
API keys are automatically wiped from memory when no longer needed.

Zeroizing<String>

use zeroize::Zeroizing;

pub struct LlmDriverConfig {
    pub provider: String,
    pub model: String,
    pub api_key: Zeroizing&lt;String&gt;,  // Auto-wipes on drop
    pub base_url: Option<String>,
}

impl Drop for LlmDriverConfig {
    fn drop(&mut self) {
        // Zeroizing&lt;String&gt; automatically overwrites memory with zeros
        // before deallocation
    }
}

Debug Redaction

impl std::fmt::Debug for LlmDriverConfig {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("LlmDriverConfig")
            .field("provider", &self.provider)
            .field("model", &self.model)
            .field("api_key", &"[REDACTED]")
            .field("base_url", &self.base_url)
            .finish()
    }
}
Prevents:
  • Memory dumps exposing secrets
  • Logs containing API keys
  • Debug output leaking credentials
Code reference: All LLM driver configs in crates/openfang-runtime/src/drivers/
The OpenFang Protocol (OFP) uses HMAC-SHA256 for peer-to-peer authentication.

Handshake Protocol

use hmac::{Hmac, Mac};
use sha2::Sha256;
use subtle::ConstantTimeEq;

type HmacSha256 = Hmac<Sha256>;

// Initiator sends:
let nonce = generate_nonce();
let data = format!("{}{}", nonce, node_id);
let mut mac = HmacSha256::new_from_slice(shared_secret.as_bytes())?;
mac.update(data.as_bytes());
let signature = mac.finalize().into_bytes();

send({
    "nonce": nonce,
    "node_id": node_id,
    "signature": hex::encode(signature)
});

// Responder verifies:
let data = format!("{}{}", msg.nonce, msg.node_id);
let mut mac = HmacSha256::new_from_slice(shared_secret.as_bytes())?;
mac.update(data.as_bytes());
let expected = mac.finalize().into_bytes();
let received = hex::decode(&msg.signature)?;

// Constant-time comparison (prevents timing attacks)
if expected.ct_eq(&received).into() {
    // Authenticated
} else {
    return Err("Authentication failed");
}

Nonce Prevents Replay Attacks

Each handshake uses a fresh random nonce. Old signatures are invalid.Code reference: crates/openfang-wire/src/auth.rs
Role-based access control — agents declare required tools, the kernel enforces it.See the Agents page for full details.TL;DR:
  • Every operation requires a capability
  • Capabilities are granted at spawn time
  • Inheritance validation prevents privilege escalation
  • Deny-by-default: if not granted, operation is blocked
Code reference: crates/openfang-kernel/src/capabilities.rs
All API responses include security headers:
use tower_http::set_header::SetResponseHeaderLayer;

let app = app
    .layer(SetResponseHeaderLayer::overriding(
        header::CONTENT_SECURITY_POLICY,
        HeaderValue::from_static(
            "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
        ),
    ))
    .layer(SetResponseHeaderLayer::overriding(
        header::X_FRAME_OPTIONS,
        HeaderValue::from_static("DENY"),
    ))
    .layer(SetResponseHeaderLayer::overriding(
        header::X_CONTENT_TYPE_OPTIONS,
        HeaderValue::from_static("nosniff"),
    ))
    .layer(SetResponseHeaderLayer::overriding(
        HeaderValue::from_static("X-XSS-Protection"),
        HeaderValue::from_static("1; mode=block"),
    ))
    .layer(SetResponseHeaderLayer::overriding(
        header::REFERRER_POLICY,
        HeaderValue::from_static("strict-origin-when-cross-origin"),
    ))
    .layer(SetResponseHeaderLayer::overriding(
        HeaderValue::from_static("Permissions-Policy"),
        HeaderValue::from_static("geolocation=(), microphone=(), camera=()"),
    ));
Code reference: crates/openfang-api/src/middleware.rs
Public health endpoint returns minimal info. Full diagnostics require authentication.

Public Health Endpoint

curl http://localhost:4200/api/health
{
  "status": "ok",
  "timestamp": "2026-03-07T12:34:56Z"
}

Authenticated Detailed Health

curl -H "Authorization: Bearer <token>" http://localhost:4200/api/health/detail
{
  "status": "ok",
  "timestamp": "2026-03-07T12:34:56Z",
  "database": {
    "connected": true,
    "size_mb": 125.4,
    "wal_size_mb": 3.2
  },
  "agents": {
    "total": 12,
    "running": 8,
    "suspended": 4
  },
  "memory": {
    "entries": 45678,
    "sessions": 23
  },
  "subsystems": {
    "kernel": "ok",
    "memory": "ok",
    "channels": "ok",
    "mcp": "ok"
  }
}
Code reference: crates/openfang-api/src/routes.rs:health()
Python/Node skill runtimes use environment isolation.

Environment Clearing

use std::process::Command;

let mut cmd = Command::new("python3");
cmd.arg("skill.py");

// Clear ALL environment variables
cmd.env_clear();

// Selectively pass safe variables
cmd.env("PATH", env::var("PATH").unwrap_or_default());
cmd.env("HOME", env::var("HOME").unwrap_or_default());
cmd.env("LANG", "en_US.UTF-8");

// Pass skill-specific env vars (from resolved settings)
for var in env_vars {
    if let Ok(value) = env::var(var) {
        cmd.env(var, value);
    }
}

// Execute
let output = cmd.output()?;
Prevents:
  • Secret leakage (subprocess can’t see parent env vars)
  • Credential theft (no AWS_*, OPENAI_API_KEY, etc. unless explicitly passed)
Code reference: crates/openfang-runtime/src/subprocess_sandbox.rs
Detects malicious patterns in skill content before activation.

Detection Patterns

const INJECTION_PATTERNS: &[&str] = &[
    "ignore previous instructions",
    "disregard all prior",
    "forget everything above",
    "your new instructions are",
    "system: ",
    "assistant: ",
    "/bin/sh",
    "/bin/bash",
    "curl http",
    "wget ",
    "rm -rf",
    "exfiltrate",
    "send this to",
    "POST https://",
];

pub fn scan_prompt_content(content: &str) -> Vec&lt;String&gt; {
    let content_lower = content.to_lowercase();
    let mut warnings = Vec::new();

    for pattern in INJECTION_PATTERNS {
        if content_lower.contains(pattern) {
            warnings.push(format!("Detected suspicious pattern: {}", pattern));
        }
    }

    warnings
}
Applied to:
  • All bundled skills (compile-time check)
  • User-installed skills (runtime check)
  • SKILL.md auto-conversion from OpenClaw
Code reference: crates/openfang-skills/src/verify.rs
SHA256-based tool call loop detection with circuit breaker.See the Agents page for full details.Code reference: crates/openfang-runtime/src/loop_guard.rs
7-phase message history validation and automatic recovery.See the Agents page for full details.Code reference: crates/openfang-runtime/src/session_repair.rs
Canonicalization with symlink escape prevention.

Safe Path Resolution

use std::path::{Path, PathBuf};

pub fn safe_resolve_path(base: &Path, user_path: &str) -> Result<PathBuf, String> {
    // Canonicalize base (resolve symlinks, make absolute)
    let base_canonical = base.canonicalize()
        .map_err(|e| format!("Base path invalid: {}", e))?;

    // Join user path to base
    let joined = base_canonical.join(user_path);

    // Canonicalize result (this resolves ../ and symlinks)
    let canonical = joined.canonicalize()
        .map_err(|e| format!("Path does not exist: {}", e))?;

    // Check that canonical path is still under base
    if !canonical.starts_with(&base_canonical) {
        return Err(format!("Path traversal detected: {}", user_path));
    }

    Ok(canonical)
}

Example Attacks Blocked

safe_resolve_path("/home/user/workspace", "../../../etc/passwd");
// Error: Path traversal detected

safe_resolve_path("/home/user/workspace", "symlink_to_root");
// Error: Path traversal detected (symlink resolved, doesn't start with base)
Code reference: crates/openfang-runtime/src/host_functions.rs
Generic Cell Rate Algorithm with cost-aware token buckets.

How It Works

use governor::{Quota, RateLimiter};
use std::num::NonZeroU32;

// Create rate limiter: 100 requests per minute
let quota = Quota::per_minute(NonZeroU32::new(100).unwrap());
let limiter = RateLimiter::direct(quota);

// Check if request is allowed
match limiter.check() {
    Ok(_) => {
        // Allow request
    }
    Err(_) => {
        // Rate limit exceeded
        return Err("Too many requests");
    }
}

Cost-Aware Limiting

Different operations consume different amounts:
// Lightweight GET request: 1 token
limiter.check_n(NonZeroU32::new(1).unwrap())?;

// LLM call: 10 tokens
limiter.check_n(NonZeroU32::new(10).unwrap())?;

// Agent spawn: 50 tokens
limiter.check_n(NonZeroU32::new(50).unwrap())?;

Per-IP Tracking

use dashmap::DashMap;
use std::net::IpAddr;

struct RateLimiterMap {
    limiters: DashMap<IpAddr, RateLimiter>,
}

impl RateLimiterMap {
    pub fn check(&self, ip: IpAddr) -> Result<(), String> {
        let limiter = self.limiters.entry(ip)
            .or_insert_with(|| RateLimiter::direct(Quota::per_minute(100)));
        limiter.check().map_err(|_| "Rate limit exceeded")?;
        Ok(())
    }
}
Code reference: crates/openfang-api/src/middleware.rs:rate_limiter()

Security Dependencies

All security systems rely on well-audited crates:
DependencyPurpose
sha2SHA-256 hashing (Merkle chain, loop guard)
hmacHMAC-SHA256 (OFP authentication)
hexHex encoding/decoding
subtleConstant-time comparison (prevents timing attacks)
ed25519-dalekEd25519 signatures (manifest signing)
randCryptographic random number generation
zeroizeMemory wiping (secret zeroization)
governorRate limiting (GCRA algorithm)
wasmtimeWASM sandbox

Capability-Based Security

The foundation of OpenFang’s security model is capability-based access control. See the Agents page for full details on:
  • Capability types
  • Pattern matching rules
  • Inheritance validation
  • Enforcement flow

Configuration

~/.openfang/config.toml
[security]
# WASM sandbox
wasm_fuel_limit = 1_000_000
wasm_epoch_timeout_seconds = 10

# Subprocess sandbox
subprocess_timeout_seconds = 60
subprocess_env_allowlist = ["PATH", "HOME", "LANG"]

# Rate limiting
rate_limit_requests_per_minute = 100
rate_limit_burst = 20

# Session repair
session_repair_enabled = true

# Loop guard
loop_guard_warn_threshold = 3
loop_guard_block_threshold = 5
loop_guard_circuit_breaker = 30

# Path traversal
allow_symlinks = false
allow_absolute_paths = false

# Audit trail
audit_trail_enabled = true
audit_trail_verify_on_load = true

Security Best Practices

Defense in Depth

No single security system is perfect. OpenFang uses 16 independent layers so that if one fails, others catch the breach.

Principle of Least Privilege

Agents are granted only the minimum capabilities they need. Default is deny-all.

Fail Secure

When an error occurs, the system defaults to the most restrictive behavior. For example:
  • Capability check fails → deny operation
  • Session repair fails → terminate agent loop
  • Path validation fails → reject file operation

Audit Everything

Every agent action is logged to the Merkle hash-chain audit trail. Tamper-evident logging means you can detect post-hoc manipulation.

Threat Model

OpenFang is designed to defend against:
ThreatDefense
Malicious agentsCapability gates + WASM sandbox + subprocess isolation
Privilege escalationInheritance validation (child ⊆ parent)
Secret leakageZeroization + env_clear() + taint tracking
SSRF attacksPrivate IP blocking + DNS resolution checks
Path traversalCanonicalization + symlink escape detection
Loop attacksLoop guard with circuit breaker
Session corruptionSession repair with 7-phase validation
Replay attacksNonce-based HMAC authentication
Timing attacksConstant-time comparison via subtle crate
Prompt injectionPrompt injection scanner
Audit tamperingMerkle hash chain
Rate limit bypassPer-IP GCRA rate limiter
XSS/clickjackingSecurity headers (CSP, X-Frame-Options, etc.)
Info disclosureHealth endpoint redaction + debug redaction
Runaway codeWASM fuel + epoch interruption + tool timeout
Memory leaksRust’s ownership system + manual memory wipe
OpenFang is not designed to defend against:
  • Physical access to the machine
  • OS-level compromise (root/admin access)
  • Side-channel attacks (Spectre, Meltdown, etc.)
  • Quantum computing attacks (Ed25519 is not post-quantum)

Reporting Security Issues

If you discover a security vulnerability in OpenFang, please report it to: Email: [email protected]
PGP Key: Download
Please include:
  • Description of the vulnerability
  • Steps to reproduce
  • Impact assessment
  • Suggested fix (if any)
We aim to respond within 24 hours and issue a patch within 7 days for critical issues.

Next Steps

Agent Capabilities

Learn about capability-based security

Architecture

Understand the full system architecture

Memory System

Explore the 6-layer memory substrate

Security Hardening Guide

Production security best practices

Build docs developers (and LLMs) love