> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/nullclaw/nullclaw/llms.txt
> Use this file to discover all available pages before exploring further.

# Security

> Multi-layer security: pairing, sandbox, workspace scoping, allowlists

## Overview

NullClaw enforces security at **every layer** of the stack:

1. **Gateway pairing** — One-time code authentication
2. **Channel allowlists** — Sender validation
3. **Workspace scoping** — Filesystem boundaries
4. **Sandbox isolation** — OS-level containment
5. **Encrypted secrets** — ChaCha20-Poly1305 AEAD
6. **Audit logging** — Signed event trail
7. **Resource limits** — Memory, CPU, disk quotas

## Security Table

| # | Item                             | Status | How                                                                                                  |
| - | -------------------------------- | ------ | ---------------------------------------------------------------------------------------------------- |
| 1 | **Gateway not publicly exposed** | Done   | Binds `127.0.0.1` by default. Refuses `0.0.0.0` without tunnel or explicit `allow_public_bind`.      |
| 2 | **Pairing required**             | Done   | 6-digit one-time code on startup. Exchange via `POST /pair` for bearer token.                        |
| 3 | **Filesystem scoped**            | Done   | `workspace_only = true` by default. Null byte injection blocked. Symlink escape detection.           |
| 4 | **Access via tunnel only**       | Done   | Gateway refuses public bind without active tunnel. Supports Tailscale, Cloudflare, ngrok, or custom. |
| 5 | **Sandbox isolation**            | Done   | Auto-detects best backend: Landlock, Firejail, Bubblewrap, or Docker.                                |
| 6 | **Encrypted secrets**            | Done   | API keys encrypted with ChaCha20-Poly1305 using local key file.                                      |
| 7 | **Resource limits**              | Done   | Configurable memory, CPU, disk, and subprocess limits.                                               |
| 8 | **Audit logging**                | Done   | Signed event trail with configurable retention.                                                      |

## 1. Gateway Pairing

### How It Works

```mermaid theme={null}
flowchart LR
    Start[nullclaw gateway] --> Generate[Generate 6-digit code]
    Generate --> Display[Display code to user]
    Display --> Wait[Wait for pairing request]
    Client[Client app] --> POST[POST /pair with code]
    POST --> Verify{Code valid?}
    Verify -->|No| Reject[Return 401]
    Verify -->|Yes| Token[Generate bearer token]
    Token --> Store[Store in ~/.nullclaw/tokens/]
    Token --> Return[Return token to client]
    Return --> Authenticated[Authenticated]
```

### Configuration

```json theme={null}
{
  "gateway": {
    "port": 3000,
    "require_pairing": true,
    "allow_public_bind": false,
    "pairing_code_ttl_secs": 300  // 5 minutes
  }
}
```

### Pairing Flow

1. **Start gateway**:
   ```bash theme={null}
   nullclaw gateway
   # → Pairing code: 123456
   ```

2. **Client pairs**:
   ```bash theme={null}
   curl -X POST http://127.0.0.1:3000/pair \
     -H "X-Pairing-Code: 123456"
   # → {"token": "tok_abc123..."}
   ```

3. **Use bearer token**:
   ```bash theme={null}
   curl http://127.0.0.1:3000/webhook \
     -H "Authorization: Bearer tok_abc123..." \
     -d '{"message": "Hello"}'
   ```

<Note>
  Pairing codes are **single-use** and expire after 5 minutes (configurable). Bearer tokens persist until revoked.
</Note>

## 2. Channel Allowlists

Every channel enforces a **sender allowlist**:

```json theme={null}
{
  "channels": {
    "telegram": {
      "accounts": {
        "main": {
          "allow_from": ["alice", "bob"]
        }
      }
    }
  }
}
```

### Allowlist Rules

* **Empty allowlist** (`[]`) → Deny all inbound messages
* **Wildcard** (`["*"]`) → Allow all (explicit opt-in)
* **Exact match** → Case-insensitive username/ID match

### Special Cases

#### Nostr

The `owner_pubkey` is **always** allowed regardless of `dm_allowed_pubkeys`:

```json theme={null}
{
  "channels": {
    "nostr": {
      "owner_pubkey": "npub1abc...",
      "dm_allowed_pubkeys": ["npub1def...", "npub1ghi..."]
    }
  }
}
```

#### Signal

Supports phone numbers and UUIDs:

```json theme={null}
{
  "channels": {
    "signal": {
      "allow_from": ["+1234567890", "uuid:a1b2c3d4-..."]
    }
  }
}
```

## 3. Workspace Scoping

### Default Behavior

All file operations are restricted to `~/.nullclaw/workspace/` by default:

```json theme={null}
{
  "autonomy": {
    "workspace_only": true,
    "allowed_paths": []  // Additional paths outside workspace
  }
}
```

### Path Validation

```zig theme={null}
pub fn validatePath(
    path: []const u8,
    workspace_dir: []const u8,
    allowed_paths: []const []const u8,
    workspace_only: bool,
) !void {
    // 1. Null byte injection blocked
    if (std.mem.indexOfScalar(u8, path, 0) != null) {
        return error.InvalidPath;
    }

    // 2. Resolve to absolute path
    const abs_path = try std.fs.realpathAlloc(allocator, path);
    defer allocator.free(abs_path);

    // 3. Check workspace boundary
    if (workspace_only) {
        if (!std.mem.startsWith(u8, abs_path, workspace_dir)) {
            // Check additional allowed paths
            for (allowed_paths) |allowed| {
                if (std.mem.startsWith(u8, abs_path, allowed)) {
                    return; // Allowed
                }
            }
            return error.PathOutsideWorkspace;
        }
    }

    // 4. Symlink escape detection (handled by realpathAlloc)
}
```

### Expanding Access

Allow specific paths outside workspace:

```json theme={null}
{
  "autonomy": {
    "workspace_only": true,
    "allowed_paths": [
      "/home/user/projects/myproject",
      "/var/log/app"
    ]
  }
}
```

Or disable entirely (not recommended):

```json theme={null}
{
  "autonomy": {
    "workspace_only": false
  }
}
```

<Warning>
  Disabling workspace scoping allows the agent to read/write **any file** the process user has access to. Use with caution.
</Warning>

## 4. Sandbox Isolation

### Supported Backends

| Backend        | OS          | Method               | Capabilities                                 |
| -------------- | ----------- | -------------------- | -------------------------------------------- |
| **Landlock**   | Linux 5.13+ | Kernel LSM           | Filesystem access control, syscall filtering |
| **Firejail**   | Linux       | Seccomp + namespaces | Full process isolation, network blocking     |
| **Bubblewrap** | Linux       | User namespaces      | Lightweight containerization                 |
| **Docker**     | Linux/macOS | Container runtime    | Full isolation, custom images                |

### Configuration

```json theme={null}
{
  "security": {
    "sandbox": {
      "backend": "auto"  // auto | landlock | firejail | bubblewrap | docker | none
    }
  }
}
```

**Auto-detection order**: Landlock → Firejail → Bubblewrap → Docker → None (fallback)

### Docker Sandbox

```json theme={null}
{
  "runtime": {
    "kind": "docker",
    "docker": {
      "image": "alpine:3.20",
      "network": "none",
      "memory_limit_mb": 512,
      "read_only_rootfs": true
    }
  }
}
```

Restrictions:

* **No network** by default
* **Read-only rootfs** (workspace mounted as writable volume)
* **Memory limit** enforced
* **Auto-removed** on exit

### Landlock Example

```zig theme={null}
const sandbox = try LandlockSandbox.init(allocator, .{
    .allowed_read_dirs = &.{ workspace_dir },
    .allowed_write_dirs = &.{ workspace_dir },
});
defer sandbox.deinit();

try sandbox.enable();
// Now process is restricted to workspace_dir only
```

## 5. Encrypted Secrets

### SecretStore

API keys are encrypted at rest with **ChaCha20-Poly1305 AEAD**:

```zig theme={null}
const SecretStore = struct {
    key_file_path: []const u8,

    pub fn encrypt(
        self: SecretStore,
        allocator: std.mem.Allocator,
        plaintext: []const u8,
    ) ![]const u8 {
        // Returns "enc2:<hex-ciphertext>"
    }

    pub fn decrypt(
        self: SecretStore,
        allocator: std.mem.Allocator,
        ciphertext: []const u8,
    ) ![]const u8 {
        // Parses "enc2:..." prefix, decrypts, verifies tag
    }
};
```

### Configuration

```json theme={null}
{
  "secrets": {
    "encrypt": true,
    "key_file": "~/.nullclaw/secret.key"  // Auto-generated if missing
  }
}
```

### Key Management

Key file format: 32-byte random key (256-bit)

```bash theme={null}
# Auto-generated on first run
ls -l ~/.nullclaw/secret.key
# -rw------- 1 user user 32 Mar 1 12:34 secret.key
```

<Warning>
  **Do NOT commit `secret.key` to git**. Backup securely. Loss of this file means encrypted secrets cannot be decrypted.
</Warning>

### Encrypted Config Fields

```json theme={null}
{
  "models": {
    "providers": {
      "openai": {
        "api_key": "enc2:a1b2c3d4..."
      }
    }
  },
  "channels": {
    "nostr": {
      "private_key": "enc2:e5f6g7h8..."
    }
  }
}
```

Decrypted at runtime, never written back in plaintext.

## 6. Audit Logging

### AuditEvent Structure

```zig theme={null}
pub const AuditEvent = struct {
    timestamp: i64,
    event_type: AuditEventType,
    actor: Actor,
    action: Action,
    result: ExecutionResult,
    context: SecurityContext,
    signature: ?[]const u8 = null,  // HMAC-SHA256 signature
};
```

### Configuration

```json theme={null}
{
  "security": {
    "audit": {
      "enabled": true,
      "retention_days": 90,
      "log_file": "~/.nullclaw/audit.log",
      "sign_events": true
    }
  }
}
```

### Example Log Entry

```json theme={null}
{
  "timestamp": 1709299200,
  "event_type": "command_execution",
  "actor": {
    "user_id": "alice",
    "channel": "telegram",
    "session_id": "sess_abc123"
  },
  "action": {
    "type": "shell",
    "command": "ls -la",
    "workspace_dir": "/home/user/.nullclaw/workspace"
  },
  "result": {
    "success": true,
    "exit_code": 0
  },
  "signature": "hmac-sha256:a1b2c3..."
}
```

### Querying Logs

```bash theme={null}
# Show recent audit events
tail -f ~/.nullclaw/audit.log | jq

# Filter by event type
jq 'select(.event_type == "command_execution")' ~/.nullclaw/audit.log

# Filter by actor
jq 'select(.actor.user_id == "alice")' ~/.nullclaw/audit.log
```

## 7. Resource Limits

### Configuration

```json theme={null}
{
  "security": {
    "resources": {
      "max_memory_mb": 512,
      "max_cpu_percent": 80,
      "max_disk_usage_mb": 1024,
      "max_subprocesses": 10
    }
  }
}
```

### Tool-Level Limits

```json theme={null}
{
  "tools": {
    "shell": {
      "timeout_secs": 60,
      "max_output_bytes": 100000
    },
    "file_read": {
      "max_file_size_bytes": 10485760  // 10 MB
    },
    "http_request": {
      "timeout_secs": 30,
      "max_response_size": 1000000  // 1 MB
    }
  }
}
```

### Enforcement

* **Shell timeout**: Subprocess killed after N seconds
* **Output truncation**: STDOUT/STDERR capped at max\_output\_bytes
* **File size**: `file_read` fails if file > max\_file\_size\_bytes
* **HTTP response**: Downloads stopped after max\_response\_size bytes

## Command Risk Classification

### Risk Levels

```zig theme={null}
pub const CommandRiskLevel = enum {
    low,     // ls, cat, echo, git status
    medium,  // curl, wget, chmod +x, npm install
    high,    // rm -rf, dd, mkfs, shutdown, reboot
};
```

### High-Risk Commands

Blocked by default (configurable):

* `rm -rf`
* `dd if=/dev/zero`
* `mkfs`
* `shutdown`
* `reboot`
* `:(){ :|:& };:`  (fork bomb)

### Configuration

```json theme={null}
{
  "autonomy": {
    "level": "supervised",  // full | supervised | restricted
    "allowed_commands": ["git", "npm", "zig"],  // Prefix matching
    "block_high_risk_commands": true,
    "require_approval_for_medium_risk": false
  }
}
```

**Autonomy levels**:

* `full` — Allow all commands (ignores `allowed_commands`)
* `supervised` — Allow `allowed_commands`, block high-risk
* `restricted` — Allow `allowed_commands` only, require approval for medium-risk

## Next Steps

<CardGroup cols={2}>
  <Card title="Configuration" icon="gear" href="/essentials/configuration">
    Full security configuration reference
  </Card>

  <Card title="Architecture" icon="diagram-project" href="/concepts/architecture">
    Learn about the security-first design
  </Card>
</CardGroup>
