arizuko › security
security
arizuko treats every agent run as untrusted code. Three controls hold today. Each turn reaches the network only through its own egress proxy, which allows a fixed list of hosts. The edge daemon proxyd checks every web login, and every backend can verify the proof it attaches. A broker keeps every credential out of the guest, except the model keys and each key that a shell:env grant names. The glossary at the end defines the terms.
The agent is the threat, so the platform walls it in
An agent can run shell commands, fetch web pages and run scripts that it wrote. Claude Code runs with bypassPermissions inside its guest, so it does not ask before it acts. A prompt injection, a bug in a tool or a hostile skill gives an attacker the same reach. The platform assumes this and limits what the guest can reach.
The main threat is cross-tenant. A compromised agent in one group tries to reach the files, secrets, messages or network of another group. Each group runs in its own KVM guest with its own share of the filesystem, its own network interface, its own view of the database and its own egress allowlist. The share is groups/<folder>/ at /home/node/, and the view comes through the MCP socket of the group. The groups solo/inbox and corp/eng are separate kernels with separate mounts. No policy inside the agent takes part, so bypassPermissions does not change this.
A fresh guest for each turn solves a different problem. Each turn boots one guest, reads one turn over vsock and powers off, so no compromised process survives into the next turn. What persists is the group folder: conversation state, diary, skills and the session transcript. The next turn loads them again. A compromised turn can therefore poison files that the next turn of the same group reads.
The operator, the chat adapters and the acl tables sit outside the agent boundary, and the platform trusts them. An adapter proves itself with a service token. proxyd checks web logins. SECURITY.md in the repository holds the trust-zone diagram and the table of boundaries. ipc/SECURITY.md covers the MCP socket and its SO_PEERCRED check.
Each turn reaches the network only through its own proxy
The guest has one network interface, a tap. The rule set of the tap accepts the proxy port of that guest and drops every other packet in both directions. runed starts one egress proxy on the host side of the tap, loads the allowlist of the folder into it, waits until its /health answers, and only then boots the guest. The proxy ends with the turn. No switch turns this off, and runed refuses to start when the egress binary is missing.
The proxy filters host names, and the tap rules confine the guest. A process that ignores HTTPS_PROXY and opens a raw socket finds every packet dropped on the host side. The proxy serves exactly one address, the address of that guest. It answers 403 to every other peer and to every host off the list. It tunnels CONNECT and never decrypts it. The egress page draws the whole path, refused branches included.
The default allowlist holds anthropic.com and api.anthropic.com. An operator adds hosts for one folder, and the folder also inherits the hosts of its parent folders:
arizuko network -i <instance> allow <folder> api.github.com
The wildcard * is added only for a folder that holds the egress grant, or for an elevated /root turn. The allowlist matches names. A row written as an IP address matches nothing and grants no access.
Model keys enter the guest, and a shell:env grant adds a folder secret
A credential takes one of two doors. The five model keys enter the environment of the guest at spawn. They are ANTHROPIC_API_KEY, ANTHROPIC_OAUTH_TOKEN, CLAUDE_CODE_OAUTH_TOKEN, OPENAI_API_KEY and CODEX_API_KEY. The agent runtime spends them, and no tool call exists to attach a broker to.
Every other credential stays on the host. When a connector call needs GITHUB_TOKEN, routd looks it up and puts it in the outbound request. The agent receives the answer with the value removed. A shell:env:<KEY> grant is the one way to put a folder secret into the shell of the guest. Infrastructure keys such as SECRETS_KEY never leave the host .env.
With the litellm package the model key stays out as well. The gateway holds it, and routd mints a MODEL_GATEWAY_KEY for each turn. AES-256-GCM encrypts the values in the secrets table at rest, and SECRETS_KEY is required. The secrets page draws the two doors.
The guest has no resolver, so no DNS filter exists
The guest carries HTTP_PROXY, HTTPS_PROXY and NO_PROXY and has no resolver at all. A host name has a meaning only inside a CONNECT line to the proxy, so a DNS query has nowhere to go. The tap drops UDP port 53 like every other packet that is not for the proxy. The proxy is the one matcher.
A tool in the guest that resolves a name itself before it connects fails. curl, wget, pip, go and npm use the proxy without help.
proxyd checks identity at the edge
Every request from outside the Docker network passes through proxyd. It ends TLS and verifies the OAuth login token, a JWT, with a refresh cookie as fallback. It strips any identity header that the client sent, adds its own and attaches a bearer token:
X-User-Sub: google:alice
X-User-Name: Alice
X-User-Groups: solo/inbox,corp/eng/sre
Authorization: Bearer <ES256 service:proxyd token>
authd mints the bearer, and its subject is pinned to service:proxyd. Every backend verifies it through auth/middleware.go. ProxydTransit checks the signature, the expiry and the subject against the public keys of authd. Only then does a backend trust the X-User-* headers. No per-request signature exists. The subject pin carries the load: without it, any holder of a valid authd token could reach a backend directly and forge X-User-*. SECURITY.md § Only proxyd stamps identity headers has the detail.
One OAuth login covers GitHub, Google and Discord. Account linking joins them under one auth_users.sub. Each route in proxyd_routes has an auth mode: public, user, operator or optional. arizuko generate writes the routes from the service manifests. /pub/* and /health are public. /chat/* and /hook/* are optional: they carry a route token, a secret in the URL, and webd checks it and limits the rate for each token.
Adapter traffic is a separate trust boundary. Channel adapters reach routd over the internal Docker network only. An adapter exchanges its bootstrap secret, AUTHD_SERVICE_KEY, for a service:<adapter> ES256 token that lasts 15 minutes. Every call to routd carries that token as the bearer. The channel gate in routd/channels.go requires a service: subject. With no verifier configured, as in local development, the gate is open. A channel name belongs to the first service identity that registers it. proxyd does not see channel traffic, and channels do not see web traffic.
REST and MCP share one handler and one gate
Every security setting is reachable two ways: secrets, allowlists, grants, routes and invites. One handler serves each resource on both.
- Outside the box, REST. Each daemon serves
/v1/<resource>for its own tables, behind the OAuth login ofproxyd.dashdbuilds the operator pages on top. - Inside the agent, MCP. The same operations are MCP tools on the unix socket of the group. A turn's tool calls use grants for both the turn's folder and the sender attested by the channel. The folder's grants apply within their scopes. The sender's allow grants add authority only for targets in the turn's folder or its subtree, and only for the exact action or its namespace wildcard. A sender's deny grant can refuse a call outside that subtree.
The position of a folder in the tree grants nothing. A folder starts with the messaging verbs and nothing else. An operation that creates state, changes an ACL or reads audit data needs a row that somebody delegated. The scope glob of that row is the boundary the folder may act inside. An action with no matching row is refused.
The MCP tool list that a session sees is filtered by the turn's folder and sender grants. A listed tool still needs authorization for its target on each call. External REST tools declared with [[connector]] and transport = "rest" join the same surface, gated on ext:<service>:<operation>.
What arizuko does not do
- No output classification. No jailbreak detector and no response scanner exist. The boundary is what the agent can reach, not what it can say.
- Encryption at rest covers secrets only. The
secretstable is encrypted. The rest of/srv/data/<instance>/is plain files, and the operator trusts the disk and the file permissions. - No SAML or OIDC login of its own. Sign-in uses GitHub, Google and Discord OAuth, and Dex when
DEX_ISSUERandDEX_CLIENT_IDare set. Invite links onboard new users. - No defense against host root. The operator owns the host. arizuko does not keep secrets from the person who runs it.
- No revocation list for tokens. The verifiers stay offline, so an access token lives out its 15 minutes. A refresh token lasts 30 days. An operator ends a login with
DELETE /v1/sessions/{family_id}, which stops the renewal. A revoked grant takes effect at the next tool call. - No IPv6 for the guest.
runedturns IPv6 off on the tap before the link comes up. The rule set of the tap covers IPv4 only. A turn whose tap cannot be sealed fails.
More detail lives in four places
- egress — the proxy of each turn: topology, flags, matching, standalone use
SECURITY.md— the full trust model, the table of boundaries, identity headers and the trust-zone diagram- secrets — the three kinds of credential and the two doors, drawn
ipc/SECURITY.md— the MCP unix socket, theSO_PEERCREDcheck and the mount isolation of each group
Glossary
- turn
- One run of an agent. A KVM guest boots, answers the new messages and powers off.
- KVM guest
- A small virtual machine that the Linux feature KVM makes. It has its own kernel, so the agent in it shares no kernel with the host.
- tap
- The virtual network cable between one guest and the host. The guest has no other network path.
- egress proxy
- A program on the host that opens outbound connections for the guest, and only to hosts on the allowlist.
- allowlist
- The host names a folder may reach. A host that is not on the list gets a
403. - group and folder
- A group is one agent with its folder under
groups/. A tenant is one group, with its own files, secrets and grants. - grant
- An
aclrow that allows or denies one action on one scope for one person or agent. - OAuth
- A sign-in through another service, such as GitHub or Google, so that no password reaches arizuko.
- bearer token
- A signed string that proves identity to whoever presents it. ES256 is the signature method.
authdsigns, and every other daemon verifies with the public keys ofauthd. - MCP
- The Model Context Protocol, the standard way an agent calls tools.
- prompt injection
- Text in a message or a web page that tricks an agent into following the instructions of an attacker.