nabeem@sre:~$ running…

Reach

Let AI agents operate your production machines - safely. Reads run; every write is blocked and queued for a human to approve before it touches anything. No SSH, no VPN, no open ports.

AI Agents MCP Claude Zero-trust access Policy engine Approval workflow Remote execution Fleet management RBAC drift Multi-tenant Go FastAPI · Lambda Postgres · DynamoDB Landlock · seccomp Kubernetes / Helm MIT
Role
Creator & maintainer
Backends
Local · Lambda · Docker · K8s
Interfaces
CLI · MCP · Web console
License
MIT (self-hosted)

Problem

Teams increasingly want AI agents to do things on real infrastructure - restart a service, scale a deployment, tail logs, roll something back. But every way of granting that access is all-or-nothing: an SSH key, a VPN, or an open port hands over broad, standing, hard-to-audit control of the box.

That's fine for a careful human. It's a serious problem for a probabilistic agent: a single hallucinated kubectl delete pods --all -n payments or rm -rf on production is unrecoverable. So "AI on prod" tends to collapse into one of two bad states - a liability (full access, fingers crossed) or blocked entirely (agents kept away from anything that matters).

Why I built it

I wanted pointing an AI at real infrastructure to be a system you can operate with confidence, not a leap of faith. The core idea is a split: reads flow freely, writes stop for a human, and everything is audited.

The subtle - and most important - part is what a human approves. In Reach you approve a structured rule, not a command string. Approving the string kubectl delete pods -n payments is dangerous because it can be extended past the check (… | tee /etc/x, … && rm -rf). Approving the rule {verb: delete, resource: pods, namespace: payments} can't be - it authorises a shape of action, once, reusably. That distinction is the difference between an agent you tolerate and one you trust.

Architecture

Reach is a control plane plus lightweight agents, driven by a CLI, an MCP server, or a web console at /ui.

  • Agents never accept inbound connections. Each agent makes outbound HTTPS requests, polls for jobs, runs them, and posts results back - no open ports, works behind NAT and egress-only firewalls. Adaptive polling tightens to ~2s during active use and relaxes to ~15s when idle.
  • The agent is a single dependency-light Go binary (standard library only - no client-go). On a host it's a systemd/launchd service sandboxed with Landlock; in Kubernetes it's a Helm Deployment whose replicas share one identity (derived from the kube-system namespace UID) with a lease-elected leader, and the token lives in a managed Secret - nothing on the pod's disk.
  • One control plane, two runtimes. A single set of handlers runs as FastAPI (Local / Docker / Kubernetes) or on AWS Lambda, over a storage split: PostgreSQL for the long-lived server, DynamoDB for serverless. Lambda + Postgres is deliberately unsupported - ephemeral connections exhaust Postgres's pool, and RDS Proxy would defeat the point of going serverless.
  • Two agent kinds. Host agents run commands directly; Kubernetes agents drive kubectl in-cluster - each enforcing policy the way that fits its substrate.
  • Fleets group host agents behind a reusable join token, so autoscaling groups (ASG / MIG / VMSS) auto-enroll on scale-out and clean up on scale-in - fast path deregister on a genuine machine shutdown (a service restart doesn't churn the record), backstop a heartbeat reaper - inheriting the fleet's mode, tags, and rules.
  • Multi-tenant throughout, with per-user read-only / read-write agent scoping enforced at the storage layer, not client-side.

Command flow (5 steps): submit (POST /jobs, annotated read/write) → the agent polls (POST /agent/sync) → executes under policy → posts the result (secrets redacted) → the CLI/MCP retrieves it. The agent has no channel back to the submitter - everything routes through the backend, and nothing is ever pushed to an agent.

Security model

Controlled execution is the whole product, so the security model is the design - two enforcement points, server-side then agent-side, that must both be bypassed:

  • No inbound, outbound-HTTPS only - the agent is never a listening service (the only exception, an optional Prometheus endpoint, is off by default).
  • Three policy modes - approved (production: reads run, every write needs a pre-approved rule), readonly (the safe default for a new agent), and wild (opt-in, personal boxes; break-glass wild can be armed with an auto-reverting duration so it can't be left open).
  • Structured rules, not strings - a host write is parsed to an argv and run with execve (no shell); approvals are JSON rules {bin, args[]} (host) or {verb, resource, namespace, name} (k8s), matched positionally with * and a trailing .... A write that needs shell features simply can't be a rule - so it's unapprovable, which is exactly why an approved action can't be extended (… | tee, … && rm -rf) to smuggle something past the check.
  • A kernel sandbox that fails closed - on Linux, reads and unapproved writes run under a Landlock read-only sandbox, so a write is blocked by the kernel, not a classifier. If the sandbox can't be applied (an old kernel, or macOS), the agent refuses to run rather than run unprotected - unless an operator explicitly, auditably acknowledges the exception.
  • Three layers on Kubernetes - RBAC (the API server's unbypassable floor) ∩ policy mode (backend, default-deny: anything not a proven read is a write) ∩ the agent's no-shell + kubectl allowlist (arguments resolving to a local file are rejected, so a job can't read its own ServiceAccount token).
  • AI can't self-approve - the MCP surface an AI drives is read-only for approvals (no create/approve/deny tool), and multi-machine fan-outs are confirm-gated behind a dry-run preview. Approval stays a human control.
  • Tokens are never stored raw - only HMAC-SHA256(pepper, token) hashes (passwords use scrypt). The agent has credential-only identity (it never sends an agent id), tokens are bound to a machine fingerprint and auto-rotate every 30 days.
  • Sensitive reads are gated like writes (SSH keys, .env, kubectl get secret); output is redacted for recognisable secrets at two layers (backend + MCP, so a secret never reaches the model), and each command runs under a timeout with capped output.
  • Verifiable supply chain - releases are signed (cosign keyless), checksummed, ship an SBOM + SLSA provenance, and are Trivy-scanned; every installer verifies its download before running it.

Honest scope: Reach is controlled, audited execution - not a sandbox for arbitrary untrusted commands, and not a boundary against the machine's own root/owner (whoever controls the host can read the agent's token). It's built to point an AI at infrastructure you own.

Engineering decisions

Poll, don't listen

Making agents outbound-only removes an entire class of exposure (no ports, no inbound auth surface) and makes deployment trivial behind firewalls - at the cost of a little polling latency, which is the right trade for infrastructure control.

Approve a rule, not a command

The structured-rule model is the security thesis. It costs a bit more friction than "approve this string once," but it's the only version where an approval can't be quietly widened.

One handler set, two runtimes

Rather than fork the backend, the same handlers run on FastAPI and on Lambda with a Postgres/DynamoDB storage split. That keeps behaviour identical across Local, Docker, Kubernetes, and serverless deployments - the logic can't drift between them.

Native AI integration via MCP

An MCP server (wired up by reach agent-init) lets Claude Code / Cursor drive a machine through Reach directly, so the safety model sits transparently under the tools people already use - and is deliberately read-only for approvals so the AI can't sign off its own request.

A dependency-light Go agent

The agent is a single Go binary on the standard library alone - no client-go - so it drops onto a host or into a cluster with a tiny footprint and no runtime to manage. The cost is re-implementing the slice of Kubernetes client logic it needs, kept honest against the backend by tests.

Credential-only agent identity

The agent never sends or stores an agent_id; it authenticates purely by a fingerprint-bound token hash. There's no id on the machine to steal or spoof, and a stolen token can't be replayed from a different host.

Trade-offs

DecisionGainedGave up
Outbound-poll agentsNo open ports; NAT/firewall-friendly; smaller attack surfacePolling latency vs. a pushed connection
Approve a ruleApprovals can't be extended past the check; reusableMore up-front friction than approving a string
Two runtimes (FastAPI + Lambda)Local, Docker, K8s and serverless from one codebaseHandlers must stay runtime- and storage-agnostic
Self-hosted onlyYour machines and audit data never leave your controlYou run and operate the backend yourself
Default readonlyA new agent is safe the moment it enrollsReal operations need a deliberate switch to approved

Challenges

  • One policy idea, two enforcement worlds. Making approved mean the same thing on a raw host (Landlock, arbitrary binaries) and inside Kubernetes (kubectl bounded by RBAC) took very different mechanisms behind one consistent mental model.
  • Expressive enough, not a language. Structured rules had to cover real operations (wildcards, trailing args, k8s verbs/resources) without turning into a policy DSL nobody wants to write.
  • Redaction vs. legitimate reads. Scrubbing secrets from output while still showing an approved secret read - because approving it is the authorisation - needed care to get right.
  • Fleets on autoscalers. Auto-enrolling on scale-out and cleaning up on scale-in, with inherited rules and drift reconciliation, so a fleet stays coherent as instances churn.
  • Keeping two runtimes honest. One set of handlers correct on both FastAPI and Lambda, over both Postgres and DynamoDB, without behaviour drift.
  • Two languages, one decision. The Go agent and Python backend both classify writes and sensitive reads - a cross-language parity test over shared golden vectors keeps them byte-for-decision identical, so an approval can never mean one thing to the backend and another to the agent. The rule/exec boundary itself is pinned by property-based and adversarial fuzz tests (a shell-bearing command can never become a structured rule).

Screenshots

Reach tenant console sign-in
Sign in - the multi-tenant console scopes every session to a tenant, with a separate platform-admin login.
Reach console: agents list
Agents - every host and Kubernetes agent, with status, policy mode, and cluster-RBAC drift at a glance.
Reach console: enrol a new agent
Enrol an agent - pick host or Kubernetes, pin the binary version, then set the execution mode (wild / read-only / approved) and scoped permissions before the join token is issued.
Reach console: jobs history
Jobs - command history across the fleet; writes in approved mode are gated (note the rejected kubectl delete).
Reach console: cluster RBAC drift
Cluster RBAC drift - an agent's effective permissions diffed against the acknowledged baseline, down to the exact verbs.
Reach console: fleets
Fleets - reusable-join-token groups of host agents; members inherit the fleet's mode, tags, and grants.

Installation example

Zero to an AI agent running commands on a real machine, in three steps:

bash
# 1. Start Reach - runs the backend, creates your tenant + first agent, installs the CLI
$ curl -fsSL https://releases.reach.nabeem.com/local-setup.sh | bash

# 2. Install the agent on the machine to control (the script prints a ready-to-paste command)
#    curl … | sudo bash   for a host   ·   helm install …   for Kubernetes

# 3. Wire your AI tool (Claude Code / Cursor) in over MCP
$ reach agent-init

# now the agent has controlled, audited access - try it:
$ reach exec -- hostname

In approved mode, a write is gated until a human signs off the rule - then it runs, this time and next:

approved mode
$ reach exec --agent prod -- kubectl delete pods --all -n payments
  Status: REJECTED - approval required; a request was sent to your operator.

# operator approves the rule once - structured, not a string:
#   { verb: delete, resource: pods, namespace: payments, name: * }
$ reach approvals approve appr_9f2c
  Approved.

$ reach exec --agent prod -- kubectl delete pods --all -n payments
  Status: SUCCEEDED

On AWS, swap step 1 for lambda-setup.sh. Docker, Kubernetes, and production hardening are covered in the repo's SELF_HOSTING.md.

Current limitations

  • Not a sandbox. Reach controls and audits execution on machines you own; it is not built to safely run arbitrary untrusted commands.
  • Not a boundary against host root. Whoever controls the host can read the agent's token - it protects against the agent, not against the owner.
  • The pepper is the crown jewel. Token hashes are useless without TOKEN_PEPPER; leak the database and the pepper together and tokens become forgeable (it also can't be rotated without reissuing everything).
  • Reads stay freeform. The blocklist is best-effort - the sandbox and RBAC are the hard floor - and shell obfuscation in wild mode / in reads is out of scope; kernel exploits below Landlock are too.
  • Self-hosted only - there's no managed SaaS; you deploy and operate the backend.
  • Early (v0.1.x) - the model is solid but the surface is still young; interfaces may move.
  • Policy is deliberately three coarse modes plus structured rules - powerful, but not a full policy engine.

Future roadmap

Directions I'm exploring (not commitments):

  • Richer, still-non-DSL rule expressiveness and finer-grained approvals.
  • More managed-storage and identity options (SSO / OIDC for the console).
  • Broader agent platforms and first-class SDKs beyond the CLI + MCP.
  • Deeper drift reconciliation and approval workflows for large fleets.

The repository is the source of truth - issues and ideas welcome.