From 6f6381242dfec708ef691e46474812a8134e0b5d Mon Sep 17 00:00:00 2001 From: Andrew Dunn Date: Tue, 10 Nov 2026 21:51:45 -0500 Subject: [PATCH] feat(api): add ENABLE_INGESTION_API_KEY to honor INGESTION_API_KEY on the app image --- .changeset/app-image-ingestion-key.md | 10 +++++ agent_docs/architecture.md | 2 +- packages/api/src/config.ts | 3 ++ .../__tests__/opampController.test.ts | 39 +++++++++++++++++++ .../src/opamp/controllers/opampController.ts | 10 ++++- packages/otel-collector/README.md | 3 +- 6 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 .changeset/app-image-ingestion-key.md diff --git a/.changeset/app-image-ingestion-key.md b/.changeset/app-image-ingestion-key.md new file mode 100644 index 0000000000..48aad189b5 --- /dev/null +++ b/.changeset/app-image-ingestion-key.md @@ -0,0 +1,10 @@ +--- +'@hyperdx/api': patch +--- + +feat: add `ENABLE_INGESTION_API_KEY` to honor `INGESTION_API_KEY` on the app image + +The app image ignores `INGESTION_API_KEY`, so an external collector has to use +the team's generated key, which changes whenever the team is recreated. Set +`ENABLE_INGESTION_API_KEY=true` on the API to accept a preset key alongside the +team key. Behavior is unchanged when unset. diff --git a/agent_docs/architecture.md b/agent_docs/architecture.md index b42eaebe5d..bac066cd48 100644 --- a/agent_docs/architecture.md +++ b/agent_docs/architecture.md @@ -105,7 +105,7 @@ pings `/v1/opamp` with status, and the server returns updated config when needed - **Model**: `models/agent.ts` - agent state persistence - **Proto**: `proto/` - Protocol Buffer definitions for OpAMP and anyvalue -Config is derived from the team document with the ingestion API key. +Config is derived from the team document with the ingestion API key. `INGESTION_API_KEY` is also accepted on all-in-one and dev builds, or anywhere with `ENABLE_INGESTION_API_KEY=true`. ### Background Tasks (`src/tasks/`) diff --git a/packages/api/src/config.ts b/packages/api/src/config.ts index 9ac7104ac9..2fb4e311c3 100644 --- a/packages/api/src/config.ts +++ b/packages/api/src/config.ts @@ -41,6 +41,9 @@ export const IS_LOCAL_IMAGE = HYPERDX_IMAGE === 'all-in-one-noauth'; export const IS_INLINE_API = env.HDX_PREVIEW_INLINE_API === 'true'; export const FRONTEND_REDIRECT_BASE = IS_INLINE_API ? '' : FRONTEND_URL; export const INGESTION_API_KEY = env.INGESTION_API_KEY ?? ''; +// Opt-in: honor INGESTION_API_KEY on the app image too, where it is ignored +// by default. The key is accepted alongside the team's key. +export const ENABLE_INGESTION_API_KEY = env.ENABLE_INGESTION_API_KEY === 'true'; // Opt-in: emit the contrib `datadogreceiver` on the collector so a // Datadog Agent can ship APM traces (DD trace API -> OTLP -> ClickHouse). // Off by default because the receiver has no per-team bearer-token auth like diff --git a/packages/api/src/opamp/controllers/__tests__/opampController.test.ts b/packages/api/src/opamp/controllers/__tests__/opampController.test.ts index dd6523c452..bedfda5dc1 100644 --- a/packages/api/src/opamp/controllers/__tests__/opampController.test.ts +++ b/packages/api/src/opamp/controllers/__tests__/opampController.test.ts @@ -5,6 +5,7 @@ const configState = { IS_LOCAL_APP_MODE: false, IS_DEV: false, INGESTION_API_KEY: '' as string, + ENABLE_INGESTION_API_KEY: false, IS_PROMQL_ENABLED: false, ENABLE_DATADOG_RECEIVER: false, }; @@ -22,6 +23,9 @@ jest.mock('@/config', () => ({ get INGESTION_API_KEY() { return configState.INGESTION_API_KEY; }, + get ENABLE_INGESTION_API_KEY() { + return configState.ENABLE_INGESTION_API_KEY; + }, get IS_PROMQL_ENABLED() { return configState.IS_PROMQL_ENABLED; }, @@ -40,6 +44,7 @@ const resetConfig = () => { configState.IS_LOCAL_APP_MODE = false; configState.IS_DEV = false; configState.INGESTION_API_KEY = ''; + configState.ENABLE_INGESTION_API_KEY = false; configState.IS_PROMQL_ENABLED = false; configState.ENABLE_DATADOG_RECEIVER = false; }; @@ -171,6 +176,40 @@ describe('opampController', () => { ); }); + it('ignores INGESTION_API_KEY outside all-in-one and dev unless opted in', () => { + configState.INGESTION_API_KEY = 'ingest-key'; + + const cfg = buildOtelCollectorConfig([ + { apiKey: 'k1', collectorAuthenticationEnforced: true }, + ]); + + const tokens = cfg.extensions['bearertokenauth/hyperdx'].tokens; + expect(tokens).toEqual(bearerTokenVariants(['k1'])); + }); + + it('includes INGESTION_API_KEY alongside the team key when ENABLE_INGESTION_API_KEY is set', () => { + configState.ENABLE_INGESTION_API_KEY = true; + configState.INGESTION_API_KEY = 'ingest-key'; + + const cfg = buildOtelCollectorConfig([ + { apiKey: 'k1', collectorAuthenticationEnforced: true }, + ]); + + const tokens = cfg.extensions['bearertokenauth/hyperdx'].tokens; + expect(tokens).toEqual(bearerTokenVariants(['k1', 'ingest-key'])); + }); + + it('adds nothing when ENABLE_INGESTION_API_KEY is set but INGESTION_API_KEY is empty', () => { + configState.ENABLE_INGESTION_API_KEY = true; + + const cfg = buildOtelCollectorConfig([ + { apiKey: 'k1', collectorAuthenticationEnforced: true }, + ]); + + const tokens = cfg.extensions['bearertokenauth/hyperdx'].tokens; + expect(tokens).toEqual(bearerTokenVariants(['k1'])); + }); + it('bearerTokenVariants emits the bare token plus the bearer scheme case variants', () => { expect(bearerTokenVariants(['tok'])).toEqual([ 'tok', diff --git a/packages/api/src/opamp/controllers/opampController.ts b/packages/api/src/opamp/controllers/opampController.ts index 13d567d966..24d6d87f48 100644 --- a/packages/api/src/opamp/controllers/opampController.ts +++ b/packages/api/src/opamp/controllers/opampController.ts @@ -182,8 +182,14 @@ export const buildOtelCollectorConfig = ( ): CollectorConfig => { const apiKeys = teams.filter(team => team.apiKey).map(team => team.apiKey); - if (config.IS_ALL_IN_ONE_IMAGE || config.IS_LOCAL_APP_MODE || config.IS_DEV) { - // Only allow INGESTION_API_KEY for dev or all-in-one images for security reasons + if ( + config.IS_ALL_IN_ONE_IMAGE || + config.IS_LOCAL_APP_MODE || + config.IS_DEV || + config.ENABLE_INGESTION_API_KEY + ) { + // Only allow INGESTION_API_KEY for dev or all-in-one images for security + // reasons, unless the deployment opts in with ENABLE_INGESTION_API_KEY if (config.INGESTION_API_KEY) { apiKeys.push(config.INGESTION_API_KEY); } diff --git a/packages/otel-collector/README.md b/packages/otel-collector/README.md index ff0077ded2..5fc6cdbc5e 100644 --- a/packages/otel-collector/README.md +++ b/packages/otel-collector/README.md @@ -157,7 +157,8 @@ config for logs). In OpAMP supervisor mode, when collector authentication is enforced the receiver authenticates against the team API keys (same as `otlp/hyperdx`) via the `DD-API-KEY` header. Set the Datadog Agent's `DD_API_KEY` to a HyperDX -team ingestion API key. +team ingestion API key, or to `INGESTION_API_KEY` where the API honors it +(all-in-one and dev builds, or with `ENABLE_INGESTION_API_KEY=true`). ## Computing span metrics from ingested traces