Security model
walkindb exposes raw SQL to unauthenticated callers. This is the document that explains why that's safe and exactly what we do to make it safe.
Threat model in one paragraph
Assume every caller is hostile. walkindb defends against: resource DoS, CPU bombs, disk bombs, filesystem escape (ATTACH '/etc/passwd'), native code execution (load_extension), cross-tenant data leakage, exfiltration / C2 via outbound network, session hijacking, and storage abuse. SQL injection is not in the threat model — callers provide raw SQL by design.
Rollout state
walkindb v0.1.0 ships the following subset of the defense-in-depth design:
| Layer | Status | Notes |
|---|---|---|
| 1. SQLite compile-time hardening | ⏳ Roadmap | Blocked on switching off modernc.org/sqlite. Application keyword blocklist + LIMIT_ATTACHED=0 + Landlock cover the concrete attacks today. |
2. sqlite3_set_authorizer callback | ⏳ Roadmap | Same blocker. Substituted with the application keyword blocklist. |
3. Per-connection sqlite3_limit | ✅ Live | Full suite: VDBE_OP=500k, SQL_LENGTH=8k, LENGTH=1M, EXPR_DEPTH=50, COMPOUND_SELECT=10, LIKE_PATTERN_LENGTH=100, VARIABLE_NUMBER=100, TRIGGER_DEPTH=5, FUNCTION_ARG=16, LIMIT_ATTACHED=0. |
| 4. Hard 2 s query timeout | ✅ Live | context.WithTimeout(2*time.Second) per call. 408 on interrupt. |
| 5. Per-instance 10 MB cap | ✅ Live | PRAGMA max_page_count = 2560. 507 on full. |
| 6a. Landlock filesystem jail | ✅ Live | Process confined to /var/walkindb/** + /tmp. open(2) for anything outside fails at the kernel level. |
| 6b. seccomp-bpf syscall allowlist | ✅ Live | via systemd SystemCallFilter=@system-service with explicit denies for @mount @swap @reboot @raw-io @cpu-emulation @debug @obsolete @privileged @resources. |
| 6c. Separate executor process + per-instance cgroups | ⏳ Roadmap | Single-process today. |
| 7. Rate and quota limits | 🟡 Partial | 60 req/min and 10 new instance creates/min per IP are live. Per-IP concurrent-instance cap, per-instance query cap, and global circuit breaker are not yet shipped. |
| 8. Session token discipline | ✅ Live | HMAC-SHA256 over UUIDv7 + 32-byte nonce. Daily secret rotation with current + previous accepted for one cycle. 404 on any verify failure. Tokens never logged. |
What this means for an attack like ATTACH '/etc/passwd'
That single attempt trips three independent layers:
- Application keyword blocklist — rejects the request with
400 {"error":"forbidden sql keyword: ATTACH"}before SQLite parses it. Comment-stripping defeats/* skip */ ATTACH; matching is case-insensitive. LIMIT_ATTACHED=0— if the blocklist is ever disabled, SQLite itself refuses to attach any databases.- Landlock — if both checks above are somehow bypassed, the kernel returns
EACCESon theopen(2)for any file outside/var/walkindb/**. The attacker cannot read/etc/passwdfrom walkindb's process no matter what SQL they run.
Same pattern applies to readfile('/etc/shadow'), load_extension('/lib/evil.so'), and every other filesystem-touching function.
What protects your walk-in from someone else's
- Every walk-in is a separate SQLite file under
/var/walkindb/instances/<uuid>/db.sqlite. - Session tokens are bound to a specific instance via HMAC. Tampering flips the hash and returns 404.
- Tokens are never logged. The structured access log stores only the resolved instance ID, not the token plaintext.
- The walkindb process runs as an unprivileged system user with no capabilities. Systemd hardens it further with
NoNewPrivileges,ProtectSystem=strict,MemoryDenyWriteExecute,RestrictNamespaces, and the rest of the standard hardened-unit boilerplate. - The TTL sweeper scans every 30 seconds and deletes expired instances. The 10-minute window limits how long a compromised session is useful.
What we log and don't log
walkindb writes exactly these seven fields per HTTP request to a JSONL access log:
timestampsource_ipinstance_id(omitted on healthz / pre-session requests)http_methodhttp_statussql_byte_length— the byte count, not the textuser_agent
We never log the SQL text, bound parameters, result rows, column names, or the session token plaintext. Access logs are retained for 7 days then deleted by an automated job. This is a public commitment in the Privacy Notice; changing it is a material policy change.
What we don't defend against
- CVEs in SQLite itself. Mitigation: aggressive patching, subscribing to
sqlite-users, and the Landlock + seccomp sandbox, which contains most escapes even if SQLite has a bug. - Side-channel timing attacks between co-located instances (Spectre/Meltdown class). Mitigation: kernel mitigations, and the 10-minute TTL makes long-lived observation windows impossible.
- PII safety. We don't claim it. The landing page and the AUP explicitly warn: do not store user PII or regulated data in a walk-in.
Bug bounty
If you find a way past any of the layers above, email [email protected]. We pay a bounty proportional to impact. Responsible-disclosure only — please don't drop 0-days on the open endpoint.
Also see
- SECURITY.md in the repo — the canonical, commit-tracked version
- Error codes — what the client sees when each layer fires
- Performance and scaling — why the single-process share-nothing architecture makes security cheaper
- Privacy Notice — public commitment on access logging