arizuko › security
arizuko treats every agent run as untrusted code. Three controls are enforced today: a per-group egress allowlist at an HTTP/HTTPS proxy on an internal: true network, OAuth-gated identity stamped at the edge with an ES256 transit bearer, and a credential broker that keeps per-user capability tokens out of the container. One control is built but not yet wired into the agent spawn: the DNS resolver that returns NXDOMAIN off the allowlist (crackbox side ships; arizuko-side --dns plumbing is follow-up). The sections below mark each one shipped or planned.
The agent is the threat surface. Claude Code runs with bypassPermissions inside its container and can shell out, fetch URLs, run generated scripts — under normal operation. Under prompt injection, RCE in a tool call, or an adversarial skill, that same scope is what the attacker gets. The platform assumes this and enforces around it.
The primary threat is cross-tenant: a compromised agent in one group reaching another group's files, secrets, messages, or network. arizuko closes that path structurally — the group is the container boundary. Each group runs in its own Docker container with its own bind mounts (groups/<folder>/ at /home/node/), its own internal: true network, its own DB view through the per-folder MCP socket, and its own egress allowlist. solo/inbox and corp/eng see none of each other — not because policy says so, but because they are separate processes on separate networks with separate mount sets. bypassPermissions inside the container doesn't change that: there is no path from the agent's shell to another group's data.
The container itself is already per-turn fresh: each agent invocation runs in its own docker run --rm container that reads one turn from stdin and exits, so no compromised process survives into the next turn. What persists across turns is the group folder under /home/node/ — conversation history, diary, skills, and the Claude Code session jsonl — re-loaded each turn so context accumulates. That persisted state, not a long-lived process, is the same-group cross-turn surface: a compromised turn could poison files the next turn reads. Per-group isolation removes the cross-tenant risk; a KVM micro-VM per turn (stronger isolation of the kernel and the persisted state) is the stricter posture for that less-common same-group threat, researched under specs/11/12-crackbox-sandboxing.md as opt-in, not default.
The system surface (operator, split daemons, store) sits outside the agent boundary. It trusts the operator, the chat-platform adapter ingress signed by CHANNEL_SECRET, and the unified acl + acl_membership tables resolved through auth.Authorize. Inbound web traffic is OAuth-gated at proxyd; proxyd proves the channel to each backend with an ES256 service:proxyd bearer and then trust-stamps the X-User-* headers, verified by every backend via auth/middleware.go.
The full trust-zone diagram and per-boundary mechanism table is in SECURITY.md in the repo. Per-daemon files ship next to the source when a daemon's threat model outgrows a row (e.g. ipc/SECURITY.md for the MCP socket and SO_PEERCRED check).
crackbox is the containment boundary for an agent run, not a proxy the agent happens to call. Every agent container attaches to a per-group Docker network created with internal: true — no default route to the internet — and the only path out is the crackbox forward proxy, set as HTTPS_PROXY in the container's environment at spawn. The agent runs inside that jail: a compromised turn reaches the allowlist and nothing else. Non-cooperating clients fail closed — a process that ignores HTTPS_PROXY finds the network unrouted. (The stronger box — the agent inside a crackbox-spawned KVM micro-VM rather than a shared-kernel container — is the next phase, § 7; today the box is container + internal network + the egress proxy below.)
Connections to non-allowlisted hosts are denied silently at the proxy. Connections to allowlisted hosts are CONNECT-tunneled (HTTPS) or forwarded (HTTP). The default seed is anthropic.com and api.anthropic.com (store/migrations/0037-network-rules.sql). Operators extend per folder:
arizuko network <instance> allow <folder> api.github.com
Allowlist resolution walks the folder ancestry and dedupes (store/network.go ResolveAllowlist). The proxy itself, packaging, and CLI live in the crackbox component page; the same daemon ships standalone for non-arizuko uses (CI scripts, vendor binaries).
Three credential types live in the secrets table, each with its own rule (specs/5/14-credential-model.md):
ANTHROPIC_API_KEY, CLAUDE_CODE_OAUTH_TOKEN, OPENAI_API_KEY, CODEX_API_KEY). User-scoped only; the store rejects them at folder scope. A user sets their own at /dash/me/env, and their key overrides the operator default for their spawns.GITHUB_TOKEN, set at /dash/me/secrets (user scope) or by the operator at folder scope. Grant-gated. When a REST or MCP connector call needs one, the broker resolves the triggering user's value on the host at call time; the token never enters the container..env (CHANNEL_SECRET, bot creds). These belong to the operator, not to any user.The agent invokes a tool by name; for a brokered call the host process resolves the keys, makes the outbound HTTP, and hands back only the response. Its own scoped secret wins over the folder default (caller's sub beats operator-managed team keys).
At rest: the secrets table stores values encrypted (AES-256-GCM, required SECRETS_KEY; see SECRETS_KEY). The surrounding rows (scope_kind ∈ {folder, user}, schema: store/migrations/0034-secrets.sql) live under /srv/data/<instance>/ like everything else, trusting disk + FS perms.
Status today: shipped. Folder secrets inject as container env at spawn (container/runner.go resolves the folder's rows and merges them into the run's env); a user's own capability key overrides the folder default. Capability credentials resolved for a REST or MCP connector call are brokered on the host and never enter the container. Values encrypted at rest (AES-256-GCM, required SECRETS_KEY). Write surfaces: /dash/me/env for model keys, /dash/me/secrets for capability tokens, the arizuko secret CLI for folder scope.
The proxy bundles a UDP/53 listener. The container's resolver points at it via docker create --dns <proxy-ip>. Per query:
1.1.1.1:53); reply validated against source addr + ID + question, then relayed.QTYPE=ANY → REFUSED regardless of allowlist (no amplification reflector).The HTTP/CONNECT enforcement is still the primary gate; DNS is additive. If a client somehow ignores both the proxy env vars and resolv.conf, the internal: true Docker network ends the conversation. Spec: specs/11/15-crackbox-dns-filter.md. Implementation: crackbox/pkg/dns/.
Status: crackbox-side DNS server shipped. arizuko-side container plumbing (passing --dns with the resolved per-folder crackbox IP) tracked as a follow-up under spec 9/10.
Every request from outside the docker network passes through proxyd. It terminates TLS, verifies the OAuth-issued JWT (refresh-token cookie fallback), strips any client-supplied identity headers, then re-stamps its own and attaches a transit bearer:
X-User-Sub: google:alice
X-User-Name: Alice
X-User-Groups: solo/inbox,corp/eng/sre
Authorization: Bearer <ES256 service:proxyd token>
The bearer is an authd-minted ES256 token whose subject is pinned to service:proxyd. Every backend verifies it via auth/middleware.go — ProxydTransit checks the signature, expiry, issuer, and that subject pin against authd's public JWKS. Only on that proof does a backend trust the stamped X-User-* headers. There is no per-request HMAC signature; the transit bearer is what makes a request a genuine “came through proxyd” call. Without the subject pin, any holder of a valid authd token reaching a backend directly could forge X-User-* and be treated as that user — covered in SECURITY.md § Identity header trust.
One OAuth login covers GitHub, Google, Discord, and Telegram — account linking unifies them under one auth_users.sub. Per-route auth modes (public / user / operator) are declared per-daemon in service config and aggregated into PROXYD_ROUTES_JSON at compose generation (specs/5/7-proxyd-standalone.md). Public paths today: /pub/*, /health, /chat/* and /hook/* (route-token bearer, per-token rate bucket).
Adapter ingress is a separate trust boundary — channel adapters reach routd over the internal docker network only, authenticated by Authorization: Bearer $CHANNEL_SECRET (chanlib/chanlib.go). proxyd does not see channel traffic; channels do not see web traffic.
The destination: every security configuration (secrets, allowlists, grants, routes, invites) is reachable two ways, sharing one handler per resource.
/v1/<resource>, OAuth-gated through proxyd. Each daemon owns its tables and exposes them; dashd aggregates the UI on top.Operations spawning new state, mutating ACLs, or reading audit data are operator-only (tier 0). Operations scoped to a group's own files, members, and tasks are inner-group (tier 1+). The per-daemon ownership table is in specs/5/5-uniform-mcp-rest.md.
Status: shipped and uniform. Each daemon exposes its resources over both surfaces from one hand-written handler — REST (/v1/<resource>, OAuth-gated through proxyd) and MCP (per-group unix socket). The MCP tools/list a session sees is filtered by grants: a tool registers only when the folder is granted its scope, so an agent sees exactly what it may call. External REST tools ([[ext]]) join the same surface, gated on ext:<service>:<operation>.
docker run --rm, one turn then exit), but it's a Docker container sharing the host kernel — not a fresh KVM micro-VM. The cross-tenant threat is closed at the group level (see § 1); stronger per-turn isolation (KVM) for the same-group threat is researched as opt-in under specs/11/12-crackbox-sandboxing.md.secrets table is encrypted (AES-256-GCM, required SECRETS_KEY; see SECRETS_KEY); the rest of /srv/data/<instance>/ is plain files. Operator trusts disk + FS perms.SECURITY.md — full trust model, boundary table, identity-header signing, trust-zone diagram, incident logspecs/9/ — security spec bucket: standalone crackbox (9), arizuko integration (10), tool-level secret broker (11), sandboxing options (12), DNS filter (15)specs/5/7-proxyd-standalone.md — per-route auth modes, federated platform APIspecs/5/1-auth-standalone.md — auth as a library: mint, verify, downscope, MCP tool surfaceipc/SECURITY.md — MCP unix socket, SO_PEERCRED peer-uid check, per-group mount isolation