Skip to content

Project credentials and the collector as the edge

Every browser, phone and backend sends to one place: the ouo11y collector, an OpenTelemetry Collector with a few components of ours. It is the only service that faces the internet, apart from Grafana. rum-api, Loki, Tempo and Mimir are internal; no client ever reaches them.

Untrustedanyone can read the bundle
Browsersweb bundle holds the token
Phonesapp binary holds the token
Your backendsenv var holds the token
People
Your teamGrafana login, SSO
HTTPS: apps with a project credential, people with a Grafana login
Public edge
collector:4318 OTLP · :4319 replay · :4320 Sentry
Grafana:3000, ouo11y RUM app
internal network only
Internalno public port, no hostname
Loki · Tempo · MimirOTLP from the collector
rum-apiingest token from the collector, read token from Grafana
Two public doors: the collector for data, Grafana for people. Everything else has no public address.

A project is one app, or one group of apps you want to tell apart: web-store, mobile-store. Its credential is a pair:

  • the project id, which must match ^[a-z0-9][a-z0-9._-]{0,63}$;
  • the project token, a random string such as pt_ followed by 32 hex characters.

Every request carries them as HTTP Basic: Authorization: Basic base64("<project id>:<project token>"). That is the standard OTLP exporter header, so any OpenTelemetry SDK can send it without ouo11y code.

The token ships inside your web bundle and your app binary, so anyone can read it. ouo11y treats it like a public API key, not a password:

  • it only lets a client write telemetry into one project;
  • within the collector’s CORS list, body size limits and rate limits;
  • it never reads anything.

Because it is public, it is checked with a fast hash ({SHA} in an htpasswd file), not bcrypt: the check runs on every request. Read more about making and rotating tokens in Project tokens.

What the collector does with every request

Section titled “What the collector does with every request”
Browser
Collector
Loki · Tempo · Mimir
rum-api
  1. 1 POST /v1/traces · no credential
  2. 2 401 · counted in ouo_auth_failures_total
  3. 3 OPTIONS preflight · Origin: https://shop.example.com
  4. 4 204, allowed · origin is in the allowed list
  5. 5 POST /v1/logs · Basic web-store:pt_…
  6. 6 check the request · per project and per client
  7. 7 stamp the project · ouo.project.id = web-store
  8. 8 OTLP export
  9. 9 POST replay chunk · :4319, same credential
  10. 10 chunk + project id · Bearer ingest token
The project id the data is stored under comes from the credential, never from the payload.
Check Default Answer when it fails
Credential required on every receiver; there is no “auth off” mode 401 + WWW-Authenticate
CORS (browsers only) http://localhost:*, http://127.0.0.1:*; one list for all projects no Access-Control-Allow-Origin; the browser blocks the request
Body size OTLP 4 MiB, replay 8 MiB (rrweb chunks 4 MiB), measured compressed and decompressed 413
Rate, per project 1000 requests per second, burst 5000 429 + Retry-After
Rate, per client address 100 requests per second, burst 300 429 + Retry-After
Memory memory_limiter at 512 MiB 503 + Retry-After

The SDKs honour Retry-After and retry. Mobile apps and servers send no Origin, so CORS does not concern them.

The project is stamped, not trusted. A resource processor deletes any ouo.project.id the client sent and writes the authenticated project id instead. Isolation downstream (filters in Grafana, retention per project, delete by project) keys on that attribute.

Rate limits are per collector instance. With two collector replicas the effective limits are twice the configured ones. Behind a reverse proxy the client is the proxy, so the per-client limit keys on the first X-Forwarded-For hop instead; use that only behind a proxy that overwrites the header, otherwise each client picks its own key. See Collector configuration.

Three tokens never leave the server side. Generate each with openssl rand -hex 16.

Secret Who presents it To whom What it unlocks
Ingest token (OUO_INGEST_TOKEN) the collector’s ouoreplay exporter rum-api POST /v1/replay/chunks writing replay chunks; nothing else
Read token (OUO_READ_TOKEN) Grafana’s plugin proxy, from the app’s secure settings rum-api GET and DELETE /v1/* reading sessions and replays
Admin token (OUO_ADMIN_TOKEN) an operator, by hand rum-api /v1/admin/* delete by user or project, the audit log; empty turns the admin API off (404)

The browser never sees the read token. The Grafana app calls /api/plugin-proxy/ouo11y-rum-app/rum/v1/... on Grafana, and Grafana adds Authorization: Bearer <read token> on the server side. Access to sessions is therefore Grafana’s access control: a Grafana user who can open the app can read every session.

The two rum-api tokens are independent: the ingest token does not read, the read token does not write. Both are compared in constant time.

Every OTLP client needs a credential, in-cluster ones too. rum-api’s own traces and metrics go through the collector like everything else. The local stack gives them a separate project, internal, so they have their own rate-limit buckets and never spend an app’s quota. Do the same for your backends if you want their limits apart from the browser’s.

  • Per-project origins. The CORS list is one list for all projects.
  • Overlapping rotation. The upstream basicauth authenticator keeps one token per project id, so rotating a token is a cut-over: clients on the old token get 401 from the moment the collector restarts. Project tokens shows how to plan for it.
  • Per-project quotas other than the shared rate-limit settings.