ADR-0079: Ingestion authentication is one gate over /ingestion/**, not per-route filters
When auth.ingestion.filter.enabled=true, a single WebFilter authenticates the whole /ingestion/** surface against a registered collector/datasource token — replacing the two exact-path filters that le
Status
Accepted (2026-06-22). Ships in 0.29.0 — resolves odd-platform#1740. Extends ADR-0002 — Centralised path-matcher authorization and ADR-0074 — Pluggable auth modes.
Context
ODD Platform has two independent authentication surfaces (see Enable security): the UI/API surface governed by auth.type, and the ingestion ingress governed by auth.ingestion.filter.enabled. The /ingestion/** namespace is in SecurityConstants.WHITELIST_PATHS, so it never traverses the central SECURITY_RULES table that ADR-0002 governs — it is protected instead by dedicated WebFilters.
Before this decision those filters bound to one exact path each: IngestionDataSourceFilter matched POST /ingestion/datasources (always on, collector-token), and IngestionDataEntitiesFilter matched POST /ingestion/entities (conditional on the flag, per-datasource token). Every other ingestion route — POST /ingestion/entities/datasets/stats, POST /ingestion/metrics, the POST /ingestion/alert/alertmanager webhook, and the GET /ingestion/entities/degs/children read — matched neither filter, so it accepted requests with no token even when auth.ingestion.filter.enabled=true. An operator who enabled the flag expecting the whole ingestion surface to require a token got only one of its routes protected.
Widening the entities filter's matcher was not viable: its body decorator parses the request as a DataEntityList to resolve the datasource oddrn, and the sibling routes carry different body shapes (DatasetStatisticsList, MetricSetList) — or, for the DEG-children read, no body at all. One body decorator cannot serve them, and adding one exact-path filter per route scatters the security model across N beans, the very pattern that let four routes ship unprotected.
Decision
Introduce one uniform IngestionAuthenticationFilter that matches /ingestion/** and, when auth.ingestion.filter.enabled=true, requires a registered collector- or datasource-Authorization: Bearer token on every ingestion route except the two that already have a dedicated filter (/ingestion/entities, /ingestion/datasources). It enforces authentication — the token must resolve to a known collector or datasource — without parsing the request body, so it covers the body-less GET read uniformly. The per-datasource authorization on /ingestion/entities (the token must match the datasource named in the body) is unchanged.
The check is eager (run before the chain proceeds, not inside a body decorator), which is why it protects the GET /ingestion/entities/degs/children read that a body-triggered check would miss. The filter is @ConditionalOnProperty("auth.ingestion.filter.enabled", havingValue="true"), mirroring the entities filter — so the flag now means what operators read it to mean: on ⇒ the whole ingestion surface needs a token.
This conforms to ADR-0002's principle of one auditable place for an authorization concern, applied to the ingestion-filter sub-system ADR-0002 leaves to the whitelist.
Consequences
With the flag on, the previously-open routes (stats, metrics, the AlertManager webhook, the DEG-children read) return 401 to an unauthenticated caller. The Enable security deployment matrix reflects this for 0.29.0.
Backward-compatible for compliant collectors.
odd-collector-sdkalready sendsAuthorization: Bearer <token>on every ingestion call, so enabling the flag does not require collector-side changes. The Prometheus AlertManager webhook is now covered too — configure Alertmanager'shttp_configto send a registered token (see Notifications and Prometheus AlertManager Integration).The shipped default is unchanged (
auth.ingestion.filter.enabled=false). With the default in place every ingestion route — including the webhook — remains open on a network-reachable deployment; that caveat is documented on the security, notifications, AlertManager-integration, and integrations pages. Whether to make the flag implicit whenauth.type != DISABLEDis a separate decision.A future
/ingestion/*route is authenticated by default — it falls under the uniform matcher rather than silently shipping open until someone remembers to add a filter.Per-resource authorization (the token must own the specific dataset) for stats and metrics is a deliberate non-goal here; this decision delivers authentication parity, and the stronger property can layer on without rework.
See also
ADR-0002 — Centralised path-matcher authorization — the auditable-rule-table principle this extends into the ingestion-filter sub-system.
ADR-0074 — Authentication is a pluggable mode selected by
auth.type— the UI/API surface; this ADR is the ingestion surface's counterpart.Enable security — the two-surface model and the per-endpoint deployment matrix.
Last updated