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 indocker-compose.yml.
Egress control and SSRF
Every outbound request from any evidence provider goes through one shared HTTP client with:
- An HTTPS-only scheme policy by default.
- An explicit host allowlist with no default wildcard — an unconfigured provider fetches nothing.
- 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.
- Response bounds: a size cap and a total time deadline.
- No proxy auto-detection —
HTTP_PROXY/HTTPS_PROXYare 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:
| Label | Excerpt retained | Sent to a model | Rendered in lenses |
|---|---|---|---|
public | Yes | Yes | Yes |
internal | Yes | Yes by default | Yes |
restricted | No — citation and metadata only | No, hard refusal | Citation 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
excerptfields 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.