Server (harnessd)

Security model

What harnessd defends against, and how: secure defaults, authentication, sessions, request handling, secrets and sandboxes.

harnessd holds your model keys, tracker tokens and repositories, and runs code written by agents. Its security model assumes the network around the server isn't fully trusted, the agents aren't trusted, and web content the agents see may be hostile.

Secure by default#

The server refuses to start unless:

  • it's bound to a specific interface, not 0.0.0.0 (unless you set allow_all_interfaces: true deliberately);
  • it has TLS 1.3, or plain HTTP only on loopback behind a local proxy, by explicit opt-in;
  • its TLS key and secrets files are mode 0600;
  • the tools listener isn't on every interface and differs from the API listener;
  • it has at least one active admin, and every admin has an authenticator if require_totp_for_admins is on.

The systemd unit runs harnessd check before every start.

Accounts and sign-in#

  • Accounts come only from the server CLI. There's no sign-up endpoint.
  • Passwords are hashed with scrypt (N=2¹⁵, r=8, p=1), with a minimum length of 12 and checks for the username, repetition and common words.
  • Optional TOTP per user, which can be required for admins.
  • No account discovery: unknown users and wrong passwords get identical responses with equal timing.
  • Lockout after max_failures failed sign-ins per account (×4 per IP) within the window.

Sessions#

  • Random 256-bit tokens, stored only as SHA-256 hashes and only in memory.
  • Idle and absolute expiry.
  • Revoked by sign-out, a password change or disabling the account.
  • Role changes apply immediately: every request checks the current account.

Authorization#

  • Three roles: viewer reads state; operator acts on tasks and runs; admin changes projects, workflows, settings and secrets. See Users, roles and codes.
  • Every route checks its own role.
  • Step-up for secrets: every change to secrets, test logins, server settings or app-added projects needs a fresh authenticator code, which can be used only once.

Requests#

  • No browser clients yet: any request with an Origin header is refused (403 origin_not_allowed), which also defends against CSRF and DNS rebinding.
  • Security headers on every response: Strict-Transport-Security, X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: no-referrer, Content-Security-Policy: default-src 'none'; frame-ancestors 'none', Cache-Control: no-store, Cross-Origin-Resource-Policy: same-origin.
  • Request bodies are capped at 1 MiB.
  • The agents' tools endpoint runs on a separate listener (tools_listen, the Docker bridge) that serves only /mcp/<session-token>. The API listener returns 404 for /mcp/.

Secrets#

  • Sandboxes never see real model keys or tracker tokens. A Docker sandbox holds a placeholder, and the egress proxy's relay adds the real credential; the server hands it to the proxy through docker exec into memory only, never onto disk, and removes it when the run ends. Cloudflare sandboxes get the same through SI Fabrik's relay.
  • Agent credentials never go into hooks or $VAR resolution, and model credential names are filtered out of docker.env.
  • Push and Cloudflare tokens are stripped from every agent and hook environment.
  • Values set from the app are write-only; only names come back.
  • Anything the server hands out (transcripts, errors, logs) is redacted of known secret values.

Sandboxes#

  • Always sandboxed. harnessd's sandbox floor refuses sandbox.provider: host, sandbox.hooks: host, docker.extra_args, docker.network and a writable root, whatever WORKFLOW.md says: saving such a workflow is refused, and a project that has one pauses with sandbox_policy. See The sandbox floor.
  • Contained. No capabilities, a read-only root filesystem, and hooks inside the sandbox. Git that touches agent-written files runs in an offline throwaway container.
  • A network per task. Each task's sandboxes share an --internal Docker network of their own. Other tasks can't reach them, and the server has no address on it, so nothing listening on the server, such as sshd, is reachable from a sandbox.
  • Default-deny egress through the allowlist proxy, which blocks cloud metadata, pins resolved addresses and checks that TLS names the allowed host. Open mode still goes through it.
  • Read-only GitHub and registries. Agents can't push to GitHub or publish to npm, crates.io or RubyGems; SI Fabrik delivers the branch itself.

See Choose a sandbox and Network policy.

Only containers this server started are listed or touched from the app, never other workloads on the same Docker.

Audit#

Logins, denials, every write action, agent tool calls and run outcomes are recorded in an append-only, hash-chained log. See Audit log.

Known limits#

  • Membership in the docker group is root-equivalent. Prefer rootless Docker and a VM dedicated to SI Fabrik.
  • An agent shares its sandbox with its browser, so it could read back a password filled in by a test login. Use test accounts only.
  • Agents with network access to an allowlisted host can send data there. Keep allowlists short.
  • The TLS name check can't see the HTTP Host header inside TLS, so a CDN that routes by that header could still carry traffic to another site it serves. Only the read-only hosts have their requests inspected.
Coming later

Per-device keys with Touch ID / Face ID step-up for dangerous actions, gVisor for sandboxes, and short-lived git tokens.

See also#