Skip to content

Threat model

Threat model

What is being protected

The telemetry itself. Logs carry request bodies and stack traces; traces carry URLs and database statements; metrics carry label values derived from user input. Treat a telemetryd data directory as containing personal data.

Defended

An operator accidentally exposing the instance. The most likely real incident: someone binds 0.0.0.0 to "just try it" and publishes their logs. telemetryd refuses to start, and the error names three fixes and generates a token to paste. This is the one case worth a long error message.

An unauthenticated network attacker. With a token configured, every ingest and query surface requires it, and so does everything on /status and /metrics that describes the deployment.

Three answers are open by design, and between them they disclose four constants of the build and nothing else: /healthz returns ok and touches nothing; / returns the identity document and the route table; and /status returns the identity document to a caller that presents no admin credential — product, version, storage_format_version, signals. No uptime, listen address, data directory, retention, record or series counts, app names, relay configuration, or per-surface auth state. A test asserts the absence of each of those by name, and the branch that chooses between the two /status documents cannot fail open: the full one is only reachable through the handler the auth guard runs after a token verifies.

Timing attacks on the token. Compared in constant time over a fixed-width hash.

Resource exhaustion by a caller. Body size, queue depth, cardinality, label lengths and attribute counts are all capped. A caller cannot drive telemetryd out of memory or mint unbounded cardinality — including in telemetryd's own metrics, which are keyed by matched route rather than raw path.

Compression bombs. Ingest accepts Content-Encoding, which means a few KB on the wire can ask for gigabytes of memory — and the request-body limit never sees it, because that limit only ever measured the compressed bytes. So the decompressed body is held to the same server.max_body_bytes through a bounded reader: output stops one byte past the cap and the request is refused, rather than being inflated and measured afterwards. Buffers are never pre-sized from a length the sender declares (gzip's trailing ISIZE is four bytes the attacker chooses), and a zstd frame's declared window — allocated before any output exists — is capped at the same limit. The bound is fuzzed as an assertion, not just tested by example.

Malformed input. The parsers are fuzzed with property tests asserting they never panic and that every failure is attributable to the client rather than reported as an internal error. That testing found a real reachable panic during development, in the lexer's handling of a backslash before a multi-byte character.

Corrupted storage. Checksums on every log record; a torn tail is repaired and reported rather than read as valid. A segment with an unreadable manifest is skipped, not misread. A format-version mismatch refuses to start rather than guessing.

Two processes on one data directory. Refused by an advisory lock.

Not defended

Anyone with read access to the data directory. There is no encryption at rest and no access control below the process. Use filesystem permissions and full-disk encryption.

A holder of the ingest token writing another app's data. app is a query namespace. Any ingest-token holder can write any app value.

A holder of the query token reading everything. There is no per-app read restriction.

Network eavesdropping. telemetryd speaks plain HTTP. Terminate TLS at a proxy.

Fingerprinting the build. / and /status publish the version to anyone who can reach the port, which turns "which build is this" into "which advisories apply". It is a decision, not an oversight, and it is not configurable — the full argument is in the configuration reference. The short version: withholding it would prevent nothing, because /healthz, the route table, the 401 shape and behavioural differences between builds already identify it; and an identity a client cannot rely on receiving is one every client must keep guessing around, which is the problem the endpoint exists to remove. If your exposure makes that trade wrong, strip the field at the reverse proxy.

A malicious operator. --insecure exists and disables the bind check. It warns loudly and is visible in /status, but it is not prevented.

Supply-chain compromise of a dependency. cargo deny gates known advisories and the SBOM makes the graph auditable, but neither stops a novel compromise.

Trust boundaries

  application  ──ingest token──▶  telemetryd  ◀──query token──  UI / operator
                                       │
                                       ▼
                              data directory
                        (filesystem permissions only)

The ingest and query boundaries are separate on purpose: app servers push, humans read, and those are different credentials with different rotation cadences. The boundary between telemetryd and its data directory is the filesystem's, not telemetryd's.

Reporting

Through GitHub's Private Vulnerability Reporting.

There is no security mailbox, PGP key or response-time commitment, because advertising a process we do not operate would be worse than saying so. Reports are read and acted on by the maintainers on a best-effort basis.