Skip to main content

Security, privacy and data boundaries

Horologium is pointed at a product's source code, issue tracker and internal documentation, and sends bounded excerpts of that content to a language model for the small slice of facts that require reading prose. The security model is designed around three specific failure modes, in this order of severity.

Credentials

  • No provider credential is ever stored in the database. Configuration stores a credential_ref — the name of an environment variable or a file path — never a value. Credentials are resolved at process start.
  • Credentials are never logged, never included in an error message, never returned by any endpoint, and never written to evidence. Presence is checked (non-empty); the value is never printed.
  • Least privilege is documented per provider: read-only tokens, scoped to the specific repository, project, or docs space.
  • The Python services (horologium-mcp, horologium-intelligence) are never issued a database credential, in any deployment topology — visible directly in docker-compose.yml.

Egress control and SSRF

Every outbound request from any evidence provider goes through one shared HTTP client with:

  1. An HTTPS-only scheme policy by default.
  2. An explicit host allowlist with no default wildcard — an unconfigured provider fetches nothing.
  3. DNS resolution checked against private/link-local ranges before every connection, and again after every redirect (up to 3 hops) — closing the DNS-rebinding gap a one-time check leaves open.
  4. Response bounds: a size cap and a total time deadline.
  5. No proxy auto-detectionHTTP_PROXY/HTTPS_PROXY are ignored unless explicitly configured, so an injected environment variable can't reroute traffic.

URLs discovered inside fetched content are never followed automatically. A documentation provider fetches only what its own configuration names — a link found inside a page is recorded as content, never fetched. This removes an entire class of content-driven SSRF rather than trying to filter it.

Sensitivity labels

Every evidence item carries a sensitivity label, set by the provider from its own configuration:

LabelExcerpt retainedSent to a modelRendered in lenses
publicYesYesYes
internalYesYes by defaultYes
restrictedNo — citation and metadata onlyNo, hard refusalCitation only, no content

restricted is a refusal, not a filter: the excerpt is never written to the database at all, so it can't later leak through an endpoint, a log, or a model call. The claim derived from it (if any) still exists and still cites its source, so a human with independent access can open it — the citation is the value.

What reaches a language model

  • Only bounded, redacted excerpt fields reach a model — never raw payloads, never full documents, never credentials, never any database row beyond the excerpt and its citation.
  • Excerpts are redacted before storage: secret-shaped tokens (private key headers, common API-key prefixes, JWT-shaped strings, other high-entropy runs matching a token pattern) are replaced with a [REDACTED:<kind>] marker in the provider, so an unredacted secret never exists in the database at all.
  • Bring your own key. No model-provider key ships with the product. With none configured, the intelligence service simply doesn't run, and inference-dependent facts resolve to Unresolved(no_provider_configured) rather than silently disabling a feature — the system is fully functional in deterministic-only mode.
  • The hosted demo sends only synthetic golden-fixture content to a model, because that's the only content it holds.
  • Model requests are logged with a prompt hash, model name, token counts, and the evidence IDs involved — never prompt or completion text.

Prompt injection

The structural defense is architectural, not prompt engineering: model output can only ever become a proposal, and a proposal is validated against existing evidence, registered property definitions, and typed values before anything is written to Product Truth. Injected instructions in a fetched documentation page can, at worst, cause a proposal that cites the page it came from — a proposal that is visible, attributed to its evidence, and rejectable. The intelligence service also has no tools of its own beyond calling the model and submitting proposals, so there's nothing for injected instructions to invoke even if they were followed.

No claim of general prompt-injection prevention is made. The claim is narrower and independently verifiable: injection cannot write to Product Truth, because nothing on that path is capable of writing to Product Truth without passing through validation first.

Web, API, and authentication — stated honestly

This Public Alpha implements no authentication of any kind: no session, no token, no user model, no roles, no per-user audit. Same-origin only, no CORS headers emitted. A self-hosted instance must not be exposed publicly without a reverse proxy in front performing real authentication — see the security section of the self-host quickstart.

There is also no tenant model: one instance holds one organization's truth, with no tenant column and therefore no row-level tenant filter to get wrong. That absence is itself a security property — the strongest available isolation between organizations is running separate instances, and it's free.

Analytics boundary

This documentation site and the public marketing surfaces may load privacy-respecting, consent-gated acquisition analytics (page views and a small, fixed set of click events) — never on the self-hosted product surface itself, and never carrying any Product Truth data, entity ID, repository or organization name, ticket key, document title, file path, or finding content. The exact analytics implementation is a separate, narrower piece of work; this page states the boundary it must respect, not the implementation.

For the full, current decision record behind everything on this page, see docs/adr/0015-security-boundaries.md in the repository.