data-olympus is pre-release software. Only the latest commit on main receives security fixes. There is no LTS branch and no backport policy yet.
| Version | Supported |
|---|---|
0.x (latest main) |
Yes |
| Earlier 0.x commits | No |
Do not open a public GitHub issue to report a security vulnerability. Public disclosure before a fix is available puts all users at risk.
Report vulnerabilities privately using GitHub's built-in Security Advisories:
- Go to
https://github.com/knaisoma/data-olympus/security/advisories/new - Fill in the advisory form with a description, reproduction steps, and (if known) a suggested fix.
- Submit. Maintainers will acknowledge within 5 business days and coordinate a fix and disclosure timeline with you.
For general questions about hardening or threat modelling, open a public Discussion instead.
We follow coordinated disclosure. Please give maintainers reasonable time (typically 90 days) to investigate and release a fix before public disclosure. We will credit reporters by name (or handle) in the advisory unless you prefer to remain anonymous.
The single-writer write pipeline (propose, pending, resolve) is the primary attack surface for a hosted deployment. Key controls:
- Path blocklist. The
KB_WRITE_BLOCK_TIERSandKB_WRITE_BLOCK_PATHSenvironment variables restrict which paths agent-proposed writes may target. Operators should configure these to the minimum surface that agents legitimately need. - Structural write rules. Only
.mdfiles under indexed prefixes are accepted. Path traversal sequences (..) and writes under.git/,tools/, or.worktrees/are rejected by the server. - Path containment. Every write path resolves its target through a shared
safe_join_under_root()guard and rejects any proposal whose resolved location escapes the per-session worktree (symlink, traversal, or absolute path), before any filesystem side effect. This covers memory propose, edit, resolve, and onboarding bootstrap. - Payload size caps.
KB_MAX_TEXT_BYTES(memory text, default 256 KiB),KB_MAX_POSTIMAGE_BYTES(edit/bootstrap file, default 1 MiB), andKB_MAX_BODY_BYTES(REST request body, default 2 MiB) reject oversized proposals before any disk write. The body cap is enforced by reading the request stream incrementally and returning413once the byte count is exceeded, so it bounds chunked orContent-Length-omitting clients, not just honest ones. Set to0to disable a given cap. - Aggregate caps. Onboarding bootstrap rejects requests over
KB_MAX_BOOTSTRAP_FILES(default 50). The pending queue is bounded byKB_PENDING_QUEUE_CAP(default 100); enqueue past the cap is rejected rather than growing unbounded on disk. - Confidence clamp. Client-supplied
confidenceis advisory. Only a caller whose principal holds theauto_commitcapability may auto-commit; everyone else has their proposals parked as pending regardless of the asserted confidence (see authentication below). - Rate limiting. A per-(remote_addr, agent_identity) sliding window (
KB_RATE_LIMIT_PER_HOUR) plus an optional per-IP cap (KB_RATE_LIMIT_PER_IP_PER_HOUR, default disabled) bound write volume. Behind a proxy, setKB_TRUSTED_PROXIESsoremote_addris the real client IP (off by default; see the network-security section). - Single writer. The server runs a single writer with advisory locking and a durable push queue. Concurrent write races from multiple agent sessions are serialised rather than silently merged.
- Secret-scanning gate (issue #71). Every commit path (
kb_propose_memory/kb_propose_editauto-commit,kb_resolve_pendingapprove, including an edited postimage, and onboarding bootstrap) scans the final postimage for credential-shaped content (PEM private-key blocks, GitHub/Slack tokens, AWS access key ids, genericpassword=/secret=assignments including env-style prefixed keys likeDB_PASSWORD=, and connection strings with an inline password) BEFORE the content-validation gate and before anything is written to disk or committed, so a validation-error message can never echo a credential value. A match on an auto-commit or bootstrap path rejects the writerejected_secret_detected. The gate also covers the fields AROUND the postimage: a credential-shapedtarget_path(filename) is rejected on edit/bootstrap/resolve without ever echoing the path back; a flagged editreasonis replaced with a redacted note before it reaches pending meta, push metadata, or audit events; a flagged memory tag is stored redacted in pending meta. A low-confidence proposal containing a secret still enters pending (so the operator keeps the override workflow below), but the scan runs at propose time too: the response never echoes the raw text (only the pattern name), and the pending entry is taggedsecret_scan_flagged/matching_pattern(names only) sokb pendingsurfaces the warning without exposing the value; a memory proposal's filename falls back to a neutral slug instead of embedding flagged text. Only the pattern name and an approximate line number are ever surfaced anywhere, never the matched value. Operators can extend the pattern set withKB_SECRET_SCAN_EXTRA_PATTERNS(comma-separated regexes; an invalid regex, or one with the classic nested-quantifier ReDoS shape, is logged and skipped, never crashes the server). Custom patterns execute through theregexengine with a hard 1-second match timeout, so even a catastrophic-backtracking pattern the load-time heuristic misses cannot hang the single-writer write path; a timed-out pattern is logged and skipped for that scan. A human resolving a pending entry may passoverride_secret_scan: true(kb resolve --override-secret-scanon the CLI) to consciously commit content the scanner flagged as a false positive; the override is recorded in the audit event and is not available on any auto-commit or bootstrap path (an agent cannot self-authorize past the gate).
Forged authority is the residual hole after the write-pipeline controls above:
under allow-by-default, a high-confidence kb_propose_edit could commit a
postimage claiming status: accepted straight into an indexed prefix, or an
edit to an already-accepted doc could change its governing content while
keeping the accepted status -- and rules propagate to the whole team via
git pull + index refresh, so one poisoned write spreads fast.
The model: agents can propose, only humans can promote. Behind
KB_GOVERNED_LANE_PROTECTION (default ON; off restores the exact
pre-#112 behavior), three mechanisms compose at the tool-function layer
(never inside the shared commit primitives, so the maintenance-ledger
system write path and the operator resolve path are untouched):
- Status clamp. A non-operator-confirmed write whose postimage sets or
changes
statusINTO the in-force class (active/accepted/approved) is demoted to a pending entry, never committed and never rejected. - Governed-target edit demotion. An edit whose target document is
CURRENTLY in force (status class AND validity window AND not-inbox AND
not-graph-excluded -- the same composed predicate every other in-force
surface uses) is always demoted to pending, regardless of confidence. An
expired or superseded-out target is not protected (it does not currently
govern anything). The in-force lookup FAILS CLOSED: when it cannot be
completed (no index, or an index read failure), the edit is demoted with
the distinct reason
governed_target_unverifiedinstead of auto-committing -- an unhealthy index cannot be leveraged to bypass this rule. An authoritative in-worktree backstop re-judges the target's CURRENT bytes on the refreshed commit base inside the serialized commit section (after the hard gates), consulting nothing from the index (a stale graph-exclusion edge could otherwise remove protection the base's own bytes assert), so an in-force doc that exists in git but is not yet re-indexed is still demoted -- index lag cannot bypass the rule in either direction. - Injection-pattern annotation. Advisory only: a postimage matching an agent-directed injection heuristic (imperative instruction-override phrasing, exfiltration-shaped URLs, base64-looking blobs, "do not tell the operator", ...) is tagged on the pending entry for the reviewer, but never blocks or demotes by itself.
Ordering: the issue #71 secret-scanning gate and the content-validation / duplicate-id gate always run FIRST. A postimage that would be rejected outright by either is rejected, never silently demoted -- a demotion cannot be used to smuggle a secret or a corrupt document past those gates by disguising it as a governance question.
Forging a governing rule now requires getting a human to approve it in
pending review, where provenance (agent identity, session, confidence,
commit trailers, the tamper-evident audit chain) is already in front of
them. The feedback loop makes a demotion hard to miss: every demotion
response carries pending_id + demotion_reason + an instruction to
inform the operator; kb_session_recap / kb session-recap / GET /api/v1/session-recap reports the committed/demoted/rejected tally for a
session; kb_consult's pending_actions envelope surfaces open demotions
for the calling session; and bin/kb-session-recap-hook is a ready-to-wire
SessionEnd/Stop hook. See docs/serving.md for the env var and demotion
semantics and docs/operations.md for the recap tooling and hook wiring.
Explicit non-goals. This is not protection against a malicious human reviewer (the operator-resolve path is, by design, the trusted promotion step) or against a git push to the remote outside this server's MCP/REST surface (branch protection and commit signing on the remote are ops-level controls, out of scope here).
Every write and enforcement decision is appended to a JSONL audit log. The log is tamper-evident: each event carries an event_id, the prev_hash of the previous event, and its own hash over the canonical event body (SHA-256, or keyed HMAC-SHA256 when KB_AUDIT_HMAC_KEY is set). Any later edit, deletion, or reordering breaks the chain; recompute it with GET /api/v1/audit/verify or kb audit --verify.
deploy/k8s/secret.template.yamlis a placeholder template only. It must never contain real keys or credentials and must never be committed with real values substituted in.- Secrets (deploy keys, API tokens) are operator-supplied at deploy time via SOPS-encrypted Kubernetes Secrets or equivalent. The codebase itself contains no credentials.
The data-olympus visualize command produces an HTML bundle graph. User-authored markdown content that ends up in the rendered output is sanitized with DOMPurify before insertion into the DOM, which mitigates stored-XSS risks from malicious frontmatter or document bodies.
The MCP server is a single-writer internal service designed for operator-controlled deployment (local network or private Kubernetes cluster).
Authentication is resolved from the Authorization: Bearer <token> header against a registry of principals, each holding a set of capabilities: read, propose, auto_commit, resolve, bootstrap, record_event.
-
KB_AUTH_TOKEN=<secret>registers a single full-capability principal namedoperator(back-compatible). -
KB_AUTH_PRINCIPALS=<json>optionally registers per-agent tokens with explicit capabilities, e.g.:[{"name": "codex", "token": "<secret>", "capabilities": ["read", "propose"]}]A principal with
proposebut notauto_commitmay propose, but its proposals are always parked as pending (the confidence clamp).Breaking change in v0.3.0: a
KB_AUTH_PRINCIPALSentry that omits thecapabilitiesfield now defaults to least privilege ({read, propose}) instead of all capabilities; grantresolve,auto_commit,bootstrap, orrecord_eventby listing them explicitly. This prevents an agent token from silently gaining self-approval (resolve+auto_commit).
Tokens are compared with hmac.compare_digest (constant time).
Coverage is REST and MCP. Unlike earlier releases where the token guarded REST routes only, an MCP-transport middleware enforces the same capabilities on the MCP write tools (kb_propose_*, kb_resolve_pending, kb_bootstrap_project, kb_record_event). The observability/enforcement tools (kb_list_pending, kb_audit, kb_consult, kb_gate_check, kb_compliance) likewise require an authenticated principal over MCP when auth is configured, matching the REST gating. A token-less MCP client can no longer call write or observability tools when auth is configured.
When auth is configured, the following require a capable principal (else 401 for an anonymous/invalid token, 403 for an authenticated principal missing the capability):
| Route / MCP tool | Capability |
|---|---|
POST /api/v1/propose/memory, /propose/edit · kb_propose_memory, kb_propose_edit |
propose (+ auto_commit to skip pending) |
POST /api/v1/resolve/{pending_id} · kb_resolve_pending |
resolve |
POST /api/v1/onboarding/bootstrap · kb_bootstrap_project |
bootstrap |
POST /api/v1/audit/event · kb_record_event |
record_event |
POST /api/v1/consult, /gate/check |
any authenticated principal |
GET /api/v1/pending, /audit, /audit/verify |
any authenticated principal |
Read routes (/api/v1/search, /get, /list, /outline, /health) remain open regardless of auth.
The bin/kb CLI includes Authorization: Bearer $KB_AUTH_TOKEN on write and observability subcommands when the variable is set.
When neither KB_AUTH_TOKEN nor KB_AUTH_PRINCIPALS is set (the default), every caller is the fully-trusted local principal and all routes are open. This is the trusted-agent assumption: only safe on a trusted private network.
Authentication is not a substitute for network-level access control. Read routes stay open, so for strict confidentiality of the knowledge base you still MUST:
- Deploy on a trusted private network, or behind an authenticating reverse proxy (terminate TLS there).
- NOT expose the server to untrusted networks.
The default kubectl apply -k deploy/k8s/ does NOT create the Ingress (it would publish the write routes to the LAN with auth unset by default); it is opt-in after KB_AUTH_TOKEN is set. The Docker Compose file binds 127.0.0.1:8080 so the local demo is not LAN-exposed. See deploy/k8s/README.md.
Proxy headers. Behind a reverse proxy, set KB_TRUSTED_PROXIES to the proxy address(es) so the rate limiter sees the real client IP via X-Forwarded-For. It is off by default (X-Forwarded-For ignored), which prevents a direct client from spoofing its address to evade the per-IP cap. See docs/serving.md.
The Kubernetes pod runs fully rootless: the prepare-git initContainer (deploy-key staging + first-boot clone) and the main container both run as uid 65534 with runAsNonRoot: true, all Linux capabilities dropped, allowPrivilegeEscalation: false, and a read-only root filesystem. The old root+gosu entrypoint phase has been removed. The base image is digest-pinned in deploy/docker/Dockerfile. Readiness targets /readyz (index loaded, independent of data staleness); /api/v1/health keeps its 503-on-degraded contract for the CLI --no-stale flag and should be alerted on, not probed. Operational runbook: docs/operations.md.
This project is licensed under the Apache License 2.0. Security notices do not change the license terms.