Permissions gateway for AI agents operating on self-hosted infrastructure
  • JavaScript 90.9%
  • Shell 9.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Andy a5f14db90c feat: per-agent operation restrictions
- Operations can specify agents[] to restrict which agents can use them
- Unscoped operations remain available to all agents (backward compatible)
- Proxmox VM/CT management scoped to specs only
2026-04-05 20:33:15 +00:00
deploy Phase 1 MVP: allowlist engine, rate limiter, audit log, executor, HTTP API 2026-04-05 13:04:28 +00:00
scripts add integration test script (9 tests: health, auth, tiers, denials, stats) 2026-04-05 13:22:38 +00:00
src feat: per-agent operation restrictions 2026-04-05 20:33:15 +00:00
.gitignore Phase 1 MVP: allowlist engine, rate limiter, audit log, executor, HTTP API 2026-04-05 13:04:28 +00:00
drawbridge.example.yaml bind to 0.0.0.0, configurable externalUrl for notification links 2026-04-05 13:28:59 +00:00
package-lock.json Phase 1 MVP: allowlist engine, rate limiter, audit log, executor, HTTP API 2026-04-05 13:04:28 +00:00
package.json Phase 1 MVP: allowlist engine, rate limiter, audit log, executor, HTTP API 2026-04-05 13:04:28 +00:00
README.md rewrite README to reflect actual implementation (all phases complete) 2026-04-05 13:20:26 +00:00
TEST-PLAN.md Update TEST-PLAN.md 2026-04-05 14:37:18 -05:00

Drawbridge

Permissions gateway for AI agents operating on self-hosted infrastructure.

Drawbridge is a lightweight HTTP service that mediates between AI agents and privileged operations (Docker, system commands, service management). Agents never get direct access to Docker sockets or sudo — they request operations through Drawbridge, which validates against an allowlist, optionally requires human approval, and maintains a full audit trail.

Inspired by the Permissions Vending Machine pattern from the ACP platform, adapted for self-hosted homelab infrastructure instead of AWS IAM.

The Problem

AI agents with Docker group membership have root-equivalent access to the host. They can:

  • docker exec into any container (read secrets, modify configs, restart services)
  • docker run -v /:/host to mount the entire host filesystem as root
  • Access password vaults, auth databases, camera feeds, and personal documents
  • Bypass all sudo restrictions via container escape

Sudo whitelists are meaningless when Docker group membership exists.

Architecture

┌─────────────────────────────────────────────────────────┐
│  OpenClaw Host                                           │
│                                                          │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐              │
│  │ Agent A  │  │ Agent B  │  │ Agent C  │              │
│  │ (token)  │  │ (token)  │  │ (token)  │              │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘              │
│       └──────────────┼──────────────┘                    │
│                      │ HTTP + Bearer auth                │
│              ┌───────▼────────┐                          │
│              │   Drawbridge   │                          │
│              │   :7600        │                          │
│              │                │                          │
│              │  ┌───────────┐ │                          │
│              │  │ Allowlist │ │  ┌──────────────────┐   │
│              │  │ Engine    │ │  │ Audit Log        │   │
│              │  └───────────┘ │  │ (SQLite)         │   │
│              │  ┌───────────┐ │  └──────────────────┘   │
│              │  │ Rate      │ │  ┌──────────────────┐   │
│              │  │ Limiter   │ │  │ Notifier         │   │
│              │  └───────────┘ │  │ (Zulip/Discord)  │   │
│              └───────┬────────┘  └──────────────────┘   │
│                      │ SSH (dedicated keypair)           │
└──────────────────────┼───────────────────────────────────┘
                       │
              ┌────────▼────────┐
              │  Docker hosts    │
              │  (remote VMs)    │
              └─────────────────┘

Features

Tiered Operations

Tier Approval Examples
Open Auto-approved docker ps, docker logs, docker inspect, df -h, free -h
Standard Auto-approved with rate limits docker restart, systemctl restart, app management commands
Sensitive Human approval required docker exec (any), reboot, config modifications
Petition Human approval + catalogue decision Any command not in the allowlist

Allowlist (not blocklist)

Every permitted operation is explicitly listed. If it's not in the allowlist, it's denied.

operations:
  - pattern: "docker ps"
    tier: open
  - pattern: "docker restart {container}"
    tier: standard
    rateLimit: "5/hour"
  - pattern: "docker exec myapp /app/manage.py {subcommand}"
    tier: standard
    allowedArgs:
      subcommand: ["migrate", "check"]
  - pattern: "docker exec {container} {command}"
    tier: sensitive

denyContainers:
  - "vaultwarden"
  - "authelia"

denyPatterns:
  - "docker run *"
  - "docker create *"
  - "docker cp *"

Hard Denials (never allowed, no override)

  • docker run / docker create / docker cp / docker build (container creation, file copy)
  • Any operation on security-critical containers (password vaults, auth gateways)
  • Host filesystem mounts

Per-Agent Authentication

Each agent gets a unique Bearer token. Tokens are validated on every request, and agents cannot impersonate each other — the token must match the claimed agent ID.

curl -X POST http://localhost:7600/ops/request \
  -H "Authorization: Bearer db_your_agent_token" \
  -H "Content-Type: application/json" \
  -d '{"agent":"main","host":"docker-host-1","command":"docker restart myapp","reason":"health check failed"}'

Generate tokens: node -e "console.log('db_' + require('crypto').randomBytes(16).toString('hex'))"

Zulip Notifications

Sensitive tier requests and petitions trigger Zulip notifications with approve/deny instructions:

🏰 Drawbridge — Approval Required

Request: req_abc123 Agent: main Host: docker-host-1 Command: docker exec myapp cat /etc/config Reason: checking configuration Tier: sensitive

React to approve, to deny.

Petitions

When an agent needs an operation not in the catalogue:

curl -X POST http://localhost:7600/ops/petition \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"agent":"specs","host":"docker-host-1","command":"apt list --upgradable","reason":"security check","suggestedTier":"open"}'

Petitions always require human approval. The approval response includes a catalogue decision:

  • Allow once — execute, don't add to catalogue
  • Allow and catalogue — execute and add to allowlist
  • Deny — reject
  • Deny and block — reject and add to deny list

Rate limited to 5 petitions/agent/day.

Config Hot-Reload

Edit drawbridge.yaml and changes take effect within 5 seconds — no restart needed. Allowlist, hosts, notifications, and agent tokens all reload automatically.

Request Expiration

Pending approval requests and petitions auto-expire after a configurable timeout (default: 15 minutes).

API

Endpoint Method Description
/ops/request POST Submit an operation for execution
/ops/status/:id GET Check status of a request
/ops/approve/:id POST Approve a pending request
/ops/deny/:id POST Deny a pending request
/ops/audit GET Query audit log (filter by agent, host, since)
/ops/stats GET Dashboard summary (counts by status, agent, tier)
/ops/petition POST Request an unlisted operation
/ops/petitions GET List petitions (filter by status)
/ops/petition/:id/decide POST Decide on a petition (allow_once, allow_catalogue, deny, deny_block)
/health GET Health check (no auth required)

Response Codes

Status Meaning
200 executed Command ran successfully
200 failed Command ran but exited non-zero
202 pending_approval Sensitive tier — waiting for human approval
403 denied Not in allowlist
403 hard_denied Matches deny pattern or denied container
429 rate_limited Too many requests for this operation
401 Missing or invalid auth token

Stack

  • Runtime: Node.js (Express)
  • Database: SQLite via better-sqlite3 (audit log, pending requests, petitions)
  • Config: YAML allowlist (drawbridge.yaml) with hot-reload
  • Execution: SSH to target hosts (configurable keypair) or local Docker socket
  • Notifications: Zulip (direct API)
  • Auth: Per-agent Bearer tokens
  • Deployment: systemd service or nohup + cron

Quick Start

# Clone and install
git clone <repo-url> drawbridge
cd drawbridge
npm ci

# Configure
cp drawbridge.example.yaml drawbridge.yaml
# Edit drawbridge.yaml: add your hosts, SSH key path, agent tokens, Zulip credentials

# Run
node src/index.js drawbridge.yaml

# Or use the control script
bash scripts/drawbridge-ctl.sh start

Security Model

What Drawbridge Stops

  • Casual privilege use. Agents use Drawbridge because it's the documented path and easier than raw SSH.
  • Accidental damage. Rate limits, allowlists, and tier classification prevent destructive loops.
  • Audit gaps. Every privileged operation is logged with agent, command, reason, and outcome.
  • Prompt injection escalation. Compromised agents are limited by the allowlist — sensitive ops require human approval, hard denials can't be overridden.
  • Agent impersonation. Per-agent tokens prevent one agent from acting as another.

Defense in Depth

Level 1 — Convention (Drawbridge as-is): Agents are instructed to use Drawbridge. All ops are audited. This handles 95% of the risk (accidental, not adversarial).

Level 2 — Filesystem hardening: Run Drawbridge as a dedicated system user. SSH key, config, and audit DB owned by root or the service user. Agents can't read the key, edit the config, or tamper with logs.

Level 3 — Network isolation: Remove agent SSH access to Docker hosts entirely. Only Drawbridge's service user has SSH keys. Agents can only reach Drawbridge's HTTP API.

Level 4 — Process isolation (OpenClaw sandbox): Each agent runs in its own Docker container via OpenClaw's built-in sandbox system. Custom Docker network routes only to Drawbridge. Full filesystem and network isolation between agents.

{
  "agents": {
    "defaults": {
      "sandbox": {
        "mode": "all",
        "backend": "docker",
        "scope": "agent",
        "docker": { "network": "drawbridge-only" }
      }
    }
  }
}

Immediate Mitigation (before Drawbridge)

Close the Docker group hole with sudoers rules:

  1. Remove the agent user from the Docker group on all hosts
  2. Create /etc/sudoers.d/agent-docker:
agent ALL=(root) NOPASSWD: /usr/bin/docker ps *
agent ALL=(root) NOPASSWD: /usr/bin/docker ps
agent ALL=(root) NOPASSWD: /usr/bin/docker logs *
agent ALL=(root) NOPASSWD: /usr/bin/docker inspect *
agent ALL=(root) NOPASSWD: /usr/bin/docker stats --no-stream *
agent ALL=(root) NOPASSWD: /usr/bin/docker restart *

No docker run, no docker exec with arbitrary commands, no volume mounts.

License

MIT