Obsidian — Hardening
Cookbook › Obsidian: Overview · Get a Vault · Core Mode · REST Setup · Hardening
Server-side defenses for any Node server exposing Alap's :obsidian: protocol. The attack surface is the HTTP server you run, not the sub-mode — so this chapter applies equally to Core and REST.
A note on tone. The guidance below reflects my understanding. I'm not a security expert — weigh this against your own threat model and consult specialists if the stakes warrant it. Alap's library safeguards are the floor; the server wrapper around them is the ceiling. I advise raising the ceiling to whatever is called for in your environment.
Library-provided REST client safeguards
REST-specific. Skim if you're running Core only. These are on by default — you get them just by using :obsidian:rest::
- Host allowlist. Only loopback addresses (
127.0.0.1,::1,localhost) by default. Non-loopback hosts require explicit opt-in viarest.allowedHosts. - TLS bypass strictly loopback-gated. Self-signed certificates accepted only on loopback. Off-loopback, verified TLS applies regardless of config flags.
- API key redaction. Every warning that mentions a URL, header, or response body runs through
redactKey()first. Keys never appear in logs. - Per-request timeout.
AbortControllerwith configurabletimeoutMs. No hung requests. - Path traversal guard. Every
/vault/{path}GET is pre-validated — empty strings, NUL bytes, leading/or\, Windows drive prefixes, and any..segment are refused without issuing a request. - Per-note failure isolation. A bad path or malformed body on one note drops that note; the rest of the batch continues.
- Body truncation cap. Each note reads up to 256 KiB (
OBSIDIAN_MAX_MATCH_BYTES). Pathologically large notes can't exhaust memory.
These are safeguards on Alap's outbound call to the plugin. They don't help with the inbound side — requests arriving at your Node server from browsers. That's what the next section is for.
The six defenses for your Node server
These apply to both Core and REST — the attack surface is the HTTP server you expose, not the sub-mode.
One scenario to think about is drive-by localhost abuse: you're running a server on localhost:9178; you browse to evil.com; evil.com's JavaScript tries fetch('http://localhost:9178/api/obsidian/query', { body: ... }) to exfiltrate your vault.
These defenses layer well; none is foolproof alone.
Bind to
127.0.0.1only, never0.0.0.0. Keeps LAN peers out.jsserver.listen(PORT, '127.0.0.1');Strict origin allowlist.
Access-Control-Allow-Originis an explicit list — never*, never reflected from theOriginheader.jsconst ALLOWED_ORIGINS = new Set(['http://localhost:9178']); if (!ALLOWED_ORIGINS.has(req.headers.origin)) return deny(res);Host header validation (DNS rebinding defense). Reject any request whose
Hostheader isn'tlocalhost:PORTor127.0.0.1:PORT. Short-TTL DNS can flip a public name to127.0.0.1after page load, which CORS alone wouldn't catch — the host-header check runs against the literal value the browser sent, so it doesn't depend on what name resolution returns.jsconst HOSTS = new Set([`localhost:${PORT}`, `127.0.0.1:${PORT}`]); if (!HOSTS.has(req.headers.host)) return deny(res);Per-query allowlist. The server holds the list of allowed queries or query shapes. A browser cannot invent
:obsidian:core:search:secrets:— only named endpoints the server registered are reachable. Turns "expression interpreter" into "named-endpoint dispatcher."jsconst ALLOWED_QUERIES = new Set([':obsidian:core:bridges:', ':obsidian:core::$small:']); if (!ALLOWED_QUERIES.has(query)) return deny(res);Capability / bootstrap token. On startup, generate a random token; serve it from a same-origin-only endpoint; require it as
Authorization: Bearer <token>on every query.jsimport { randomBytes } from 'node:crypto'; const BOOTSTRAP_TOKEN = randomBytes(32).toString('hex'); // GET /alap/bootstrap → returns token (only to same-origin callers) // Every /api/* request checks Authorization headerPer-boot rotation bounds the blast radius: a leaked token dies on server restart. Open tabs see a single 401 and re-fetch.
Shared secret on top of the above — an extra pre-configured secret the page must also know, in addition to the bootstrap token. Belt-and-suspenders.
Layered together, each defense protects against a different failure mode. CORS (step 2) limits which origins the browser will let talk to the server. The host-header check (step 3) doesn't depend on DNS — it inspects the literal Host value. Same-origin storage (step 5) keeps the bootstrap token unreadable from other origins even if the earlier layers are circumvented.
Additional hardening (not required; worth knowing)
- Randomize the port. Server picks an ephemeral port on boot, writes it to a file or prints it. A predictable port like
:9178is easier to pre-target than one chosen at startup. - Audit log. Append
origin + query + timestampto a local file for every query. Trivial to grepevil.comafter the fact. - Rate limiting. Cap queries/sec per origin.
- Idle token expiration (optional). Tokens dropped after a window of inactivity.
- Redaction hooks.
protocols.obsidian.redact: ['^secret_', '^.*_private$']— filter matched frontmatter keys or filenames from output. Defense in depth for specific files you never want to surface.
Auditing an existing install
When reviewing a project for :obsidian: exposure:
grep -r "alap/protocols/obsidian"— find the import sites.- Is the consumer a build-time tool (SSG) or a runtime server?
- If runtime — which of the six defenses does it apply? Any missing?
- Is
vaultPathor the REST endpoint configured? Without it, the handler is inert.
See also
- REST Setup — installing the Local REST API plugin and wiring it to Alap
- Core Mode — filesystem-only sub-mode
- Security reference — full per-feature security model
- Threat model — what the library does, doesn't do, and assumes from integrators