Skip to content

Project tokens and rotation

A project credential is a project id and a public token, sent as HTTP Basic on every request. Why it can be public, and what it allows: Project credentials. This page is the procedures.

Terminal window
echo "pt_$(openssl rand -hex 16)"

The collector reads projects from an htpasswd file, one line per project: <project id>:{SHA}<base64 of the SHA-1 of the token>. With htpasswd (package apache2-utils):

Terminal window
htpasswd -nbs web-store pt_9f3c…

Without it, the same line with openssl:

Terminal window
printf '%s:{SHA}%s\n' web-store "$(printf '%s' pt_9f3c… | openssl dgst -binary -sha1 | openssl base64)"

Use {SHA} or apr1, never bcrypt: the hash runs on every request. Project ids must match ^[a-z0-9][a-z0-9._-]{0,63}$.

Append the line to projects.htpasswd (Deploy with Docker Compose), then restart the collector. It reads the file once, at start:

Terminal window
docker compose restart otel-collector

Check it: 401 before the restart, 200 after.

Terminal window
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://otel.example.com/v1/traces -u "web-store:pt_9f3c…" -H 'Content-Type: application/json' -d '{"resourceSpans":[]}'
initRUM({
endpoint: "https://otel.example.com",
projectId: "web-store",
projectToken: "pt_9f3c…",
service: { name: "web-store", version: "1.4.2", environment: "production" },
});

Any OpenTelemetry SDK:

Terminal window
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic%20$(printf 'web-store:pt_9f3c…' | base64)"

%20 is how the OTLP environment variables write the space after Basic.

You
Collector
Old clients
New clients
  1. 1 new token, new build
  2. 2 replace line, restart
  3. 3 Basic …:pt_old
  4. 4 401 · invalid_credentials
  5. 5 Basic …:pt_new
  6. 6 200
A cut-over. On the web it is a deploy away; on mobile, old app versions stay on the old token until users update.
  1. Make the new token and build your app with it, but do not release it yet.

  2. Replace the project’s line in the htpasswd file with one for the new token, and restart the collector as in Add a project.

  3. Release the app. Web apps switch on the next page load. Watch the old token fade out:

    sum by (reason) (rate(ouo_auth_failures_total[5m]))

For mobile apps, a cut-over cuts off every installed version that has not updated. Rotate to a new project id instead: add mobile-store-2 with its own token next to mobile-store, ship the new app version with it, and delete the old line once the old versions are gone. Both ids are valid at once. In Grafana, filter on both projects meanwhile.

Delete the project’s line and restart the collector. Every request with that credential gets 401 from then on. Data already stored stays; to delete it too, use rum-api’s admin API (rum-api).

These are never in a client, so rotating them is a restart, not a release.

Secret Set the new value on Then
ingest token the collector and rum-api (OUO_INGEST_TOKEN; Helm: ingest-token in the rum-api Secret) restart both. The collector retries chunks that rum-api refused in between for up to 5 minutes
read token rum-api and Grafana (OUO_READ_TOKEN, provisioned into the app’s secure settings; Helm: read-token) restart both. Or change it on the app’s configuration page in Grafana, /plugins/ouo11y-rum-app, without a Grafana restart
admin token rum-api (OUO_ADMIN_TOKEN; Helm: admin-token) restart rum-api
internal project token the htpasswd file and rum-api’s OTEL_EXPORTER_OTLP_HEADERS (OUO_BACKEND_OTLP_HEADERS; Helm: otlp-headers) restart both, or rum-api’s own telemetry is refused with 401

In Docker Compose, docker compose up -d after editing .env recreates exactly the containers whose variables changed. In Helm, a change to a chart-rendered Secret rolls the pods that use it; for a Secret you manage, run kubectl rollout restart.