Skip to content

Security

AEndrix edited this page May 15, 2026 · 2 revisions

Security

graft is local-first; the default surface is the AF_UNIX socket only. Enabling HTTP, remote bind, or encryption-at-rest is opt-in. This page documents the threat model and what's implemented.

Threat model

Implicit trust boundaries:

  • The user account that owns ~/.graft/ is trusted.
  • The local AF_UNIX socket is reachable only by processes with 0600 access (forced).
  • The HTTP layer, when enabled, is the only network surface.

Out of scope:

  • Multi-user shared host without per-user profiles. Use per-user GRAFT_HOME.
  • Untrusted code running as the graft user. Treat your DB as you treat any other personal SQLite file.

AF_UNIX socket

  • Path forced 0600 at creation.
  • The socket file is unlinked at daemon shutdown.
  • No auth on the socket — anyone with FS access to it is trusted.

SQLite database

  • File mode forced 0600 on POSIX.
  • WAL mode; the -wal / -shm files inherit the same mode.

Encryption at rest (opt-in)

Build the daemon against SQLCipher (not the default), then:

export GRAFT_DB_KEY="<passphrase>"
graft stats

The daemon applies PRAGMA key + PRAGMA cipher_compatibility = 4 at every open. Without the SQLCipher build, the env var is a no-op (logged at startup so you notice).

HTTP surface

Loopback-only by default

bind = 127.0.0.1. The daemon refuses to start with a non-loopback bind unless:

  1. http.allow_remote: true AND
  2. Either http.auth_token is set OR http.tls_terminated_externally: true.

This makes "accidentally exposed to the internet" impossible without an explicit operator decision.

Authentication

  • http.auth_token — Bearer token for full access. Compared in constant time. Env GRAFT_HTTP_AUTH_TOKEN overrides the config field, useful for systemd units.
  • http.readonly_token — Optional second token, GETs only. POST/DELETE return 403.
  • GET /v1/healthz — always unauthenticated (container probes).

CSRF defense

When auth_token is set and a write reaches /v1/insert or /v1/nodes/{id} DELETE, the daemon validates the Origin (or Referer) header against an allow-list. Browsers always send Origin; CLI clients (curl, fetch from Node, daemon-to-daemon) skip the check naturally.

Per-endpoint kill switch

Each endpoint has its own toggle. Useful patterns:

  • Public viewer dashboard: endpoint_insert: false, endpoint_delete: false, endpoint_view: true, readonly_token: <viewer-team-token>.
  • Headless ingest worker: endpoint_view: false, endpoint_insert: true, full auth_token only.

Process model

  • The daemon drops privileges only to the user account it runs as. No setuid.
  • llama.cpp runs in-process. CPU-bound. No outbound network from the daemon (the BGE-M3 model download happens via the CLI on first install).
  • The HTTP listener is Mongoose (vendored). No raw socket handling in our code.

Known limitations

  • HTTP Host-header validation — DNS-rebinding mitigation via Host validation is not currently enforced in code. If you're binding 0.0.0.0, put a real reverse proxy in front and rely on its Host filtering.
  • Rate limiting — none in graft itself. Add it at the proxy if you expose /v1/*.
  • Audit log — there's a ~/.graft/usage.jsonl usage log; it isn't tamper-evident.

Reporting

Email security issues to the maintainer privately (see GitHub profile). Don't open public issues for vulnerabilities.

Clone this wiki locally