Horopter

Credentials and Capabilities

What a credential grants, how it is scoped to a tenant, and what Horopter leaves to you.

Every route on Horopter's two daemons, except the health probes, needs a credential. This page explains what a credential is, what each role may do, and which protections are yours to provide. It describes Horopter v0.14.2.

Credentials

A credential is a key a producer or reader presents as a bearer token: Authorization: Bearer horo_<prefix>_<secret>. Horopter stores only a hash of the secret, so a key is shown once, when it is issued, and cannot be recovered afterwards. The prefix names the key in logs and is what you give to revoke it.

A credential carries, besides its role:

  • a tenant, or none: see Tenants;
  • a source, the name of the system it was issued for, such as github;
  • a locus, the first part of every ref it writes, which defaults to the source: see What Is Horopter;
  • optionally a subject pattern, which limits the subjects its events may name.

Credentials are issued, listed and revoked with horoctl, directly against Horopter's database; see Issue Credentials. A revoked credential is refused by new requests within 30 seconds, the time a daemon caches a credential it has checked.

Roles and Capabilities

A credential is issued as a role, and a role is a stored set of capabilities. A route needs one capability:

capabilitylets a credential
appendpost events to ingress
readread changes and events from egress, through its own API and its adapters
adminrepair a change, by re-folding it from its events
issuereserved for issuing credentials over the API, which Horopter does not have yet

Four roles are seeded:

rolecapabilitiesfor
producerappenda system that records changes, such as a CI job
readerreada tool that shows changes, such as Grafana
tenant_adminread, adminan operator of one tenant
superuserappend, read, admin, issuean operator of every tenant

Give each producer and each reader its own credential with the narrowest role that does its job. A producer key that leaks lets its holder post events; a reader key lets its holder read a tenant's changes. Only horoctl, with access to the database, issues credentials.

A subject pattern narrows append only: an event naming a subject outside the pattern is refused. Reading and repairing changes are whole-tenant, so horoctl refuses a subject pattern on any role that grants more than append.

Tenants

A tenant is a partition of Horopter's data: one tenant's events and changes are never served to another's credentials. A credential is scoped to one tenant, or to none:

  • A tenant-scoped credential acts in its own tenant only. A producer's events land in its credential's tenant, and an event that names a tenant is refused. A reader may name its own tenant with ?tenant= and no other.
  • A global credential has no tenant, and names the tenant it reads with ?tenant= on every request.

Refusals

answerwhen
401no credential, or one that is malformed, unknown, revoked or expired
403a valid credential without the route's capability, or naming another tenant

Every 401 has the same body, whatever the reason. Telling a revoked key from an unknown one would let a caller probe for valid prefixes.

A read that fails inside Horopter is answered 500 with a generic body. The underlying error, which can name tables and the database host, goes to the daemon's log instead.

What Horopter Leaves to You

  • The database is the root of trust. Whoever can reach Postgres can read and write the event log and the credential table, past every check the daemons make. horoctl needs no key of its own for the same reason: its authority is database access. Keep the database reachable only by Horopter and its operators.
  • Floods of requests. Nothing in Horopter limits how many requests arrive; see Read Admission. Put a WAF or equivalent in front of both daemons.
  • The metrics listener is unauthenticated. It binds localhost by default. Bound to another interface, it shows metric names, route paths and your OTEL_RESOURCE_ATTRIBUTES to anyone who reaches it, though no tenant, event or database identity.
  • The health probes are open. /healthz and /readyz need no credential, so an orchestrator can probe them. Point a liveness probe at /healthz and a readiness probe at /readyz: the images have no shell, and carry no HEALTHCHECK.
  • Development stacks are for one machine. The compose file a release ships issues a producer and a reader key that everyone knows, and never a superuser. Do not run it anywhere its ports are reachable from another machine, or reuse its keys.

On this page