Skip to content

Commit 559d98c

Browse files
authored
feat(datadog): validate API keys at runtime to track forwarder health (#1841)
## Summary <!-- Please provide a brief summary about what this PR does. This should help the reviewers give feedback faster and with higher quality. --> ADP now supports runtime API key refresh through a configurable `u64` key (which defaults to `60 minutes`). Things to note: - Additional endpoints added during runtime will not be accounted for since it would involve dynamically altering the topology by creating a new forwarder and related components to support the new endpoint. This _also_ mimics Core Agent behavior. - Non-zero integer values fall back to the default (`60`) - Fake API key is not treated as invalid - Forwarders' health now dictated by whether at least **one** API-key is valid. - `200` is the valid status code - `403` is invalid - ADP will treat unexpected status codes as errors and not invalid keys. - Integration tests will now use the fake API key (32-char string of all 0s) ## Change Type - [ ] Bug fix - [x] New feature - [ ] Non-functional (chore, refactoring, docs) - [ ] Performance ## How did you test this PR? <!-- Please how you tested these changes here --> Unit tests ## References <!-- Please list any issues closed by this PR. --> <!-- - Closes: <issue link> --> - Closes #1357 <!-- Any other issues or PRs relevant to this PR? Feel free to list them here. --> Co-authored-by: lucas.tembras <lucas.tembras@datadoghq.com>
1 parent f651b72 commit 559d98c

35 files changed

Lines changed: 802 additions & 69 deletions

File tree

docs/agent-data-plane/configuration/dogstatsd.md

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ tracking.
3030
| `dogstatsd_experimental_http.listen_address` | Bind address for experimental HTTP DSD listener | [#1682] |
3131
| `dogstatsd_pipe_name` | Windows named pipe path | [#1466] |
3232
| `dogstatsd_windows_pipe_security_descriptor` | Windows named pipe ACL descriptor | [#1466] |
33-
| `forwarder_apikey_validation_interval` | API key check interval (minutes) | [#1357] |
3433
| `serializer_experimental_use_v3_api.compression_level` | V3 API zstd compression level | [#1468] |
3534
| `serializer_experimental_use_v3_api.series.endpoints` | Endpoints enabled for V3 series API | [#1468] |
3635
| `serializer_experimental_use_v3_api.series.shadow_sample_rate` | V3 series shadow mode sample rate | [#1468] |
@@ -158,18 +157,19 @@ deployment path, ADP does not implement `heroku_dyno`. See [#1753].
158157
The following settings are recognized by both ADP and the core agent, but with different behavior or
159158
default values.
160159

161-
| Config Key | Description |
162-
| ---------------------------------- | ----------------------------------------- |
163-
| `dogstatsd_mapper_cache_size` | Mapper result LRU cache size |
164-
| `dogstatsd_metrics_stats_enable` | Enable per-metric debug stats |
165-
| `forwarder_high_prio_buffer_size` | High-priority request queue size |
166-
| `forwarder_num_workers` | Concurrent forwarder workers |
167-
| `log_level` | Log verbosity directives |
168-
| `min_tls_version` | Minimum TLS version for HTTPS connections |
169-
| `multi_region_failover.enabled` | Enable multi-region failover mode |
170-
| `serializer_zstd_compressor_level` | Zstd compression level |
171-
| `skip_ssl_validation` | Skip TLS cert validation |
172-
| `statsd_forward_host` | UDP packet forwarding destination host |
160+
| Config Key | Description |
161+
| -------------------------------------- | ----------------------------------------- |
162+
| `dogstatsd_mapper_cache_size` | Mapper result LRU cache size |
163+
| `dogstatsd_metrics_stats_enable` | Enable per-metric debug stats |
164+
| `forwarder_apikey_validation_interval` | API key check interval (minutes) |
165+
| `forwarder_high_prio_buffer_size` | High-priority request queue size |
166+
| `forwarder_num_workers` | Concurrent forwarder workers |
167+
| `log_level` | Log verbosity directives |
168+
| `min_tls_version` | Minimum TLS version for HTTPS connections |
169+
| `multi_region_failover.enabled` | Enable multi-region failover mode |
170+
| `serializer_zstd_compressor_level` | Zstd compression level |
171+
| `skip_ssl_validation` | Skip TLS cert validation |
172+
| `statsd_forward_host` | UDP packet forwarding destination host |
173173

174174
### `dogstatsd_mapper_cache_size`
175175

@@ -191,6 +191,20 @@ mapper, clear `dogstatsd_mapper_profiles` instead when running ADP.
191191

192192
See `dogstatsd_stats_enable`
193193

194+
### `forwarder_apikey_validation_interval`
195+
196+
ADP supports `forwarder_apikey_validation_interval` for Datadog intake forwarding. The value is in minutes,
197+
defaults to `60`, and falls back to that default for non-positive values.
198+
199+
ADP validates configured API keys when the forwarder starts, periodically at the configured interval, and
200+
after API-key-related runtime config updates. Validation controls readiness only: confirmed invalid keys can
201+
make the forwarder not ready, but the forwarder keeps running and transient validation failures do not prove
202+
that keys are invalid.
203+
204+
Support is partial because ADP only tracks `additional_endpoints` domains present at forwarder startup.
205+
Runtime updates can rotate keys for those domains, but brand-new domains are not added or validated until
206+
restart.
207+
194208
### `forwarder_high_prio_buffer_size`
195209

196210
ADP implements `forwarder_high_prio_buffer_size` and matches the core agent's default of `100`. The

lib/datadog-agent/config-testing/src/config_registry/forwarder.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,17 @@ crate::declare_annotations! {
313313
test_json: None,
314314
pipeline_affinity: PipelineAffinity::CrossCutting,
315315
};
316+
/// `forwarder_apikey_validation_interval`-API key check interval (minutes)
317+
FORWARDER_APIKEY_VALIDATION_INTERVAL = SalukiAnnotation {
318+
schema: &schema::FORWARDER_APIKEY_VALIDATION_INTERVAL,
319+
support_level: SupportLevel::Partial,
320+
additional_yaml_paths: &[],
321+
env_var_override: None,
322+
used_by: &[structs::FORWARDER_CONFIGURATION],
323+
value_type_override: Some(ValueType::Integer),
324+
test_json: None,
325+
pipeline_affinity: PipelineAffinity::CrossCutting,
326+
};
316327
/// `forwarder_max_concurrent_requests`-Max concurrent HTTP requests
317328
FORWARDER_MAX_CONCURRENT_REQUESTS = SalukiAnnotation {
318329
schema: &schema::FORWARDER_MAX_CONCURRENT_REQUESTS,

lib/datadog-agent/config-testing/src/config_registry/unsupported.rs

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -82,17 +82,6 @@ crate::declare_annotations! {
8282
test_json: None,
8383
pipeline_affinity: PipelineAffinity::Pipelines(&[Pipeline::DogStatsD]),
8484
};
85-
/// `forwarder_apikey_validation_interval`-API key check interval (minutes)
86-
FORWARDER_APIKEY_VALIDATION_INTERVAL = SalukiAnnotation {
87-
schema: &schema::FORWARDER_APIKEY_VALIDATION_INTERVAL,
88-
support_level: SupportLevel::Incompatible(Severity::Medium),
89-
additional_yaml_paths: &[],
90-
env_var_override: None,
91-
used_by: &[],
92-
value_type_override: None,
93-
test_json: None,
94-
pipeline_affinity: PipelineAffinity::CrossCutting,
95-
};
9685
/// `forwarder_flush_to_disk_mem_ratio`-Mem-to-disk flush threshold
9786
FORWARDER_FLUSH_TO_DISK_MEM_RATIO = SalukiAnnotation {
9887
schema: &schema::FORWARDER_FLUSH_TO_DISK_MEM_RATIO,

lib/datadog-agent/config/schema/schema_overlay.yaml

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1143,13 +1143,27 @@ inventory:
11431143
config_registry_filename: encoders.rs
11441144

11451145
forwarder_apikey_validation_interval:
1146-
support: none
1147-
severity: medium
1148-
planned: true
1146+
support: partial
11491147
pipelines: [cross_cutting]
11501148
description: "API key check interval (minutes)"
1151-
documentation: "Core agent supports runtime API key refresh without restart. ADP has partial infrastructure (config-side listeners) but the forwarder does not act on updates."
1149+
documentation: |
1150+
ADP supports `forwarder_apikey_validation_interval` for Datadog intake forwarding. The value is in minutes,
1151+
defaults to `60`, and falls back to that default for non-positive values.
1152+
1153+
ADP validates configured API keys when the forwarder starts, periodically at the configured interval, and
1154+
after API-key-related runtime config updates. Validation controls readiness only: confirmed invalid keys can
1155+
make the forwarder not ready, but the forwarder keeps running and transient validation failures do not prove
1156+
that keys are invalid.
1157+
1158+
Support is partial because ADP only tracks `additional_endpoints` domains present at forwarder startup.
1159+
Runtime updates can rotate keys for those domains, but brand-new domains are not added or validated until
1160+
restart.
11521161
issue: "#1357"
1162+
test_support:
1163+
used_by: [ForwarderConfiguration]
1164+
value_type_override: integer
1165+
additional_attributes:
1166+
config_registry_filename: forwarder.rs
11531167

11541168
forwarder_backoff_base:
11551169
support: full

lib/datadog-agent/config/src/classifier/classifier_data.rs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -291,13 +291,6 @@ pub(crate) static CLASSIFIER_ENTRIES: &[ClassifierEntry] = &[
291291
pipeline_affinity: PipelineAffinity::Pipelines(&[Pipeline::DogStatsD]),
292292
default: Some("\"\""),
293293
},
294-
ClassifierEntry {
295-
yaml_path: "forwarder_apikey_validation_interval",
296-
aliases: &[],
297-
support_level: SupportLevel::Incompatible(Severity::Medium),
298-
pipeline_affinity: PipelineAffinity::CrossCutting,
299-
default: Some("60"),
300-
},
301294
ClassifierEntry {
302295
yaml_path: "heroku_dyno",
303296
aliases: &[],

lib/saluki-components/src/common/datadog/config.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ const fn default_forwarder_connection_reset_interval() -> u64 {
3333
0
3434
}
3535

36+
const fn default_api_key_validation_interval_mins() -> u64 {
37+
60
38+
}
39+
40+
const fn default_api_key_validation_interval_config_mins() -> i64 {
41+
default_api_key_validation_interval_mins() as i64
42+
}
43+
3644
const MIN_TLS_VERSION_TLS10: &str = "tlsv1.0";
3745
const MIN_TLS_VERSION_TLS11: &str = "tlsv1.1";
3846
const MIN_TLS_VERSION_TLS12: &str = "tlsv1.2";
@@ -259,6 +267,18 @@ pub struct ForwarderConfiguration {
259267
/// outbound intake request. The data plane does not perform local tag validation based on this setting.
260268
#[serde(default)]
261269
allow_arbitrary_tags: bool,
270+
271+
/// API key validation interval, in minutes.
272+
///
273+
/// All values that are less than or equal to zero will be ignored, and the default
274+
/// value will be used.
275+
///
276+
/// Defaults to 60 minutes.
277+
#[serde(
278+
default = "default_api_key_validation_interval_config_mins",
279+
rename = "forwarder_apikey_validation_interval"
280+
)]
281+
api_key_validation_interval_mins: i64,
262282
}
263283

264284
impl ForwarderConfiguration {
@@ -267,6 +287,15 @@ impl ForwarderConfiguration {
267287
let mut forwarder_config = config.as_typed::<Self>()?;
268288
forwarder_config.parsed_min_tls_version = min_tls_version_from_config_value(&forwarder_config.min_tls_version);
269289

290+
if forwarder_config.api_key_validation_interval_mins <= 0 {
291+
warn!(
292+
config_key = "forwarder_apikey_validation_interval",
293+
fallback_minutes = default_api_key_validation_interval_mins(),
294+
"Configured API key validation interval is invalid; using default."
295+
);
296+
forwarder_config.api_key_validation_interval_mins = default_api_key_validation_interval_mins() as i64;
297+
}
298+
270299
// Handle fixing up the forwarder storage path if it's empty.
271300
forwarder_config.retry.fix_empty_storage_path(config);
272301

@@ -407,6 +436,11 @@ impl ForwarderConfiguration {
407436
pub const fn allow_arbitrary_tags(&self) -> bool {
408437
self.allow_arbitrary_tags
409438
}
439+
440+
/// Returns the API key validation interval.
441+
pub const fn api_key_validation_interval(&self) -> Duration {
442+
Duration::from_mins(self.api_key_validation_interval_mins as u64)
443+
}
410444
}
411445

412446
#[cfg(test)]
@@ -583,6 +617,32 @@ mod tests {
583617
assert_eq!(config.endpoint_concurrency(), 12);
584618
}
585619

620+
#[tokio::test]
621+
async fn api_key_validation_interval_parsing() {
622+
let cases = [
623+
("missing", serde_json::json!({}), Duration::from_mins(60)),
624+
(
625+
"positive",
626+
serde_json::json!({ "forwarder_apikey_validation_interval": 5i64 }),
627+
Duration::from_mins(5),
628+
),
629+
(
630+
"zero",
631+
serde_json::json!({ "forwarder_apikey_validation_interval": 0i64 }),
632+
Duration::from_mins(60),
633+
),
634+
(
635+
"negative",
636+
serde_json::json!({ "forwarder_apikey_validation_interval": -1i64 }),
637+
Duration::from_mins(60),
638+
),
639+
];
640+
641+
for (case_name, extra_config, expected_interval) in cases {
642+
let config = forwarder_config_from(config_with(extra_config), None).await;
643+
assert_eq!(config.api_key_validation_interval(), expected_interval, "{case_name}");
644+
}
645+
}
586646
#[tokio::test]
587647
async fn skip_ssl_validation_defaults_to_false() {
588648
let config = forwarder_config_from(base_config(), None).await;

lib/saluki-components/src/common/datadog/endpoints.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,11 @@ impl RoutableEndpoint {
280280
&self.endpoint
281281
}
282282

283+
/// Returns the resolved endpoint mutably.
284+
pub(crate) const fn endpoint_mut(&mut self) -> &mut ResolvedEndpoint {
285+
&mut self.endpoint
286+
}
287+
283288
/// Consumes the routable endpoint and returns its parts.
284289
pub(crate) fn into_parts(self) -> (EndpointRoute, ResolvedEndpoint) {
285290
(self.route, self.endpoint)

lib/saluki-components/src/common/datadog/io.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ use super::{
4040
middleware::{for_resolved_endpoint, with_allow_arbitrary_tags, with_version_info},
4141
telemetry::{ComponentTelemetry, SharedTransactionQueueTelemetry, TransactionQueueTelemetry},
4242
transaction::{Metadata, Transaction, TransactionBody},
43+
validation::ApiKeyValidator,
4344
METRIC_INTAKE_PATHS,
4445
};
4546

@@ -234,6 +235,16 @@ where
234235
io_shutdown_rx,
235236
}
236237
}
238+
239+
/// Returns API key validation for the startup endpoint set.
240+
pub(crate) fn api_key_validator(&self) -> ApiKeyValidator {
241+
ApiKeyValidator::new(
242+
self.endpoints.clone(),
243+
self.client.clone(),
244+
self.live_config.clone(),
245+
self.config.api_key_validation_interval(),
246+
)
247+
}
237248
}
238249

239250
#[allow(clippy::too_many_arguments)]

lib/saluki-components/src/common/datadog/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub mod request_builder;
99
mod retry;
1010
pub mod telemetry;
1111
pub mod transaction;
12+
pub mod validation;
1213

1314
use saluki_core::data_model::event::trace::{AttributeValue, Trace};
1415
use stringtheory::MetaString;

0 commit comments

Comments
 (0)