Security Rules
These are the security rules that govern the LTG. They are enforced at multiple layers: the daemon, the libs, the CLI, and Postgres. Each rule is testable and auditable.
R1 — Zero trust between transport and authorization
- R1.1 — Loopback presence is not authentication.
- R1.2 — Tailscale presence is not authentication.
- R1.3 — A Supabase JWT alone is insufficient for trust elevation; a signed envelope with a registered device public key is also required.
- R1.4 — The daemon refuses to accept signed envelopes whose
kidis not adevices.registered_devices.idbelonging to the JWT-authenticated Lenser. - R1.5 —
0.0.0.0and::binds are forbidden (preconditionbind_safe). - R1.6 — Non-loopback binds (Tailscale, WireGuard meshes) require an explicit on-disk consent record at
~/.lenserfight/gateway/tailscale-consent.jsongranted vialf gateway consent grant tailscale. The consent pins the live interface fingerprint (name:cidr-or-address); a mismatch refuses startup withtailscale_consent: fingerprint_mismatch. CGNAT is detected as100.64.0.0/10per RFC 6598.
R2 — Least privilege
- R2.1 —
service_roleis never present inapps/gateway/runtime config orapps/cli/runtime config. - R2.2 — All sensitive table mutations go through
SECURITY DEFINERRPCs withSET search_path(existing pattern insupabase/migrations/20270511200000_devices_schema.sql). - R2.3 — All new tables ENABLE ROW LEVEL SECURITY by default; no policy means no access.
- R2.4 — All new RPCs declare
GRANT EXECUTE ... TO authenticated(orservice_role) explicitly. - R2.5 —
xp.applyremains granted toservice_roleonly; new XP-minting paths route through DEFINER RPCs that themselves callxp.apply.
R3 — Signed envelopes
- R3.1 — Canonical shape:
{ v: 1, alg: 'ed25519', kid, iat, nonce, body, sig }. - R3.2 — Canonicalization: JCS (RFC 8785).
- R3.3 — Hash: SHA-256 over the JCS output of
{v, alg, kid, iat, nonce, body}. - R3.4 — Signature: detached Ed25519, base64url.
- R3.5 — Verification is performed in Postgres (
fn_verify_attestation_signature) usingpgsodium, OR by the Edge Functionsupabase/functions/verify-attestation/. Never by an untrusted client. - R3.6 — A signed envelope is single-use across the replay window.
R4 — Replay protection
- R4.1 —
iatwindow: ±300 seconds from server clock. - R4.2 —
nonceis 128-bit cryptographically random, base64url-encoded. - R4.3 —
devices.nonce_cacheretains nonces for 600 seconds; replays are rejected withnonce_replay. - R4.4 — Daemon
lf gateway doctor --check clockMUST pass before any signed RPC is sent.
R5 — Secret handling
- R5.1 — Private keys live in the OS keychain via
libs/utils/keychain. They are never written to:- the database,
~/.lenserfight/lenserfight.json,- any project-level file (
.lenserfight.json,.lenserfight/*), - environment variables.
- R5.2 — BYOK API keys remain in env or OS keychain per
libs/providers/src/lib/byok-key-resolver.ts. The daemon never logs them. - R5.3 — Supabase
service_rolekey, when present in development, is read only from env. The daemon refuses to start if it sees one (service_role_in_daemon). - R5.4 — Profile JSON files (
~/.lenserfight/profiles/*.json) remain mode0600. Daemon-side state goes under~/.lenserfight/gateway/with the same file mode.
R6 — Kill switch propagation
- R6.1 —
agents.workspace_settings.global_kill_switch = truecauses the daemon to refuse to start and the CLI to refuse new executions. - R6.2 — Device
trust_level ∈ {revoked, blocked, unhealthy}causes the daemon to refuse to sign attestations on behalf of that device. - R6.3 — Workspace
runner_paused = true(canonical column name; seerequirements.md§RP) causes the daemon to refuse new executions for that workspace. - R6.4 — Daemon polls policy state every 10 s; CLI polls on every command unless cached < 60 s.
R7 — Audit chain
- R7.1 — Device trust transitions (
pending → approved → trusted → revoked → ...) are appended toaudit.hash_chainswithchain_kind = 'gateway'. - R7.2 — Sync outbox flushes are appended in batches with one hash per batch.
- R7.3 — Daemon lifecycle events (start, stop, key rotation, refusal causes) are appended.
- R7.4 — Audit chain entries are append-only (enforced by
public.fn_deny_mutation). - R7.5 — Verification helpers expose
chain_verify(p_lenser_id UUID)for incident response.
Example query (read-only):
SELECT created_at, kind, device_id, prev_hash, payload_hash
FROM audit.hash_chains
WHERE chain_kind = 'gateway'
AND lenser_id = $1
ORDER BY created_at DESC
LIMIT 200;R8 — Defense in depth
Every trust elevation crosses at least three independent layers:
- Signature — verified server-side.
- DB policy — RLS + DEFINER RPC.
- Audit trigger — append-only, hash-chained.
A single layer failure cannot silently elevate trust. For example, even if a malicious daemon manages to call a DEFINER RPC, the signature verification rejects the call before any row is written.
R9 — Anti-cheat
- R9.1 — Clients may not set
gateway_verified = truedirectly. Server overwrites the field after signature verification. - R9.2 — Clients may not set
device_trusted = true. Server reads fromdevices.registered_devices.trust_levelat attestation time. - R9.3 — Clients may not set
policy_passed = true. Server reads the latestagents.policy_evaluationsverdict for the run. - R9.4 —
execution_verifiedandfully_trustedtrust levels require server-set fields only. - R9.5 — Battle reputation pipelines consume only server-derived levels.
R10 — Localhost exposure
- R10.1 — Daemon binds
127.0.0.1by default. Never0.0.0.0. - R10.2 — Adding a non-loopback bind requires an explicit CLI flag (
--tailscale, future--bind). - R10.3 — Doctor verifies that no public network interface accidentally hosts the daemon.
R11 — Config ownership
- R11.1 —
.lenserfight.json(project-level, commit-safe): NO secrets, NO tokens, NO keys. ExistingProjectConfigshape inapps/cli/src/config/project-config.tsis authoritative. - R11.2 —
~/.lenserfight/lenserfight.json(user-level): tokens only; NEVER signing keys. - R11.3 —
~/.lenserfight/gateway/state.json(daemon-level): non-secret runtime state (last heartbeat, current peer roster cache). - R11.4 — OS keychain: signing keys, optionally BYOK keys.
- R11.5 — Environment: bootstrap secrets only (Supabase URL, anon key, optional dev tokens).
R12 — Workflow trust
- R12.1 — A workflow run reaches
fully_trustedonly when both the workflow definition and its lens version are hashed into the attestation envelope (workflow_hash,lens_hash). - R12.2 — Workflows fetched at runtime are pinned to a specific
lenses.versions.id. Mutations to a referenced version invalidate any in-flight attestation. - R12.3 — Tool invocations within a workflow are themselves logged to
agents.tool_invocationsand contribute to the trust factorpolicy_passed.
Doctor checklist
lf gateway doctor is the single command that verifies most of the above:
--check clock— clock skew within 5 minutes of Supabase.--check keychain— keychain backend reachable; can read/write a smoke entry.--check identity— Ed25519 keypair present; not older than rotation policy.--check daemon—lf-gatewaydreachable on its bind address; healthy.--check sync— outbox depth below threshold; watermarks fresh; no unresolved conflicts.--check policy—global_kill_switch,runner_pausedstate; daemon not in degraded mode.--check transport— no public bind; if--tailscale, tailnet interface detected.
Doctor exit code is non-zero on any failed check; CI runs lf gateway doctor --check clock,identity,daemon after every PR that touches apps/gateway, apps/cli, libs/infra/gateway, supabase/migrations/**, or supabase/functions/**.