Skip to content

Commit bae97ec

Browse files
authored
[SVLS-7934] feat: Support TLS certificate for trace/stats flusher (#961)
## Problem A customer reported that their Lambda is behind a proxy, and the Rust-based extension can't send traces to Datadog via the proxy, while the previous go-based extension worked. ## This PR Supports the env var `DD_TLS_CERT_FILE`: The path to a file of concatenated CA certificates in PEM format. Example: `DD_TLS_CERT_FILE=/opt/ca-cert.pem`, so the when the extension flushes traces/stats to Datadog, the HTTP client created can load and use this cert, and connect the proxy properly. ## Testing ### Steps 1. Create a Lambda in a VPC with an NGINX proxy. 2. Add a layer to the Lambda, which includes the CA certificate `ca-cert.pem` 3. Set env vars: - `DD_TLS_CERT_FILE=/opt/ca-cert.pem` - `DD_PROXY_HTTPS=http://10.0.0.30:3128`, where `10.0.0.30` is the private IP of the proxy EC2 instance - `DD_LOG_LEVEL=debug` 4. Update routing rules of security groups so the Lambda can reach `http://10.0.0.30:3128` 5. Invoke the Lambda ### Result **Before** Trace flush failed with error logs: > DD_EXTENSION | ERROR | Max retries exceeded, returning request error error=Network error: client error (Connect) attempts=1 DD_EXTENSION | ERROR | TRACES | Request failed: No requests sent **After** Trace flush is successful: > DD_EXTENSION | DEBUG | TRACES | Flushing 1 traces DD_EXTENSION | DEBUG | TRACES | Added root certificate from /opt/ca-cert.pem DD_EXTENSION | DEBUG | TRACES | Proxy connector created with proxy: Some("http://10.0.0.30:3128") DD_EXTENSION | DEBUG | Sending with retry url=https://trace.agent.datadoghq.com/api/v0.2/traces payload_size=1120 max_retries=1 DD_EXTENSION | DEBUG | Received response status=202 Accepted attempt=1 DD_EXTENSION | DEBUG | Request succeeded status=202 Accepted attempts=1 DD_EXTENSION | DEBUG | TRACES | Flushing took 1609 ms ## Notes This fix only covers trace flusher and stats flusher, which use `ServerlessTraceFlusher::get_http_client()` to create the HTTP client. It doesn't cover logs flusher and proxy flusher, which use a different function (http.rs:get_client()) to create the HTTP client. However, logs flushing was successful in my tests, even if no certificate was added. We can come back to logs/proxy flusher if someone reports an error.
1 parent ebaddff commit bae97ec

File tree

8 files changed

+121
-15
lines changed

8 files changed

+121
-15
lines changed

bottlecap/Cargo.lock

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bottlecap/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ sha2 = { version = "0.10", default-features = false }
3838
hex = { version = "0.4", default-features = false, features = ["std"] }
3939
base64 = { version = "0.22", default-features = false }
4040
rustls = { version = "0.23.18", default-features = false, features = ["aws-lc-rs"] }
41+
rustls-pemfile = { version = "2.0", default-features = false, features = ["std"] }
42+
rustls-pki-types = { version = "1.0", default-features = false }
43+
hyper-rustls = { version = "0.27.7", default-features = false }
4144
rand = { version = "0.8", default-features = false }
4245
prost = { version = "0.13", default-features = false }
4346
zstd = { version = "0.13.3", default-features = false }

bottlecap/LICENSE-3rdparty.csv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ rustc-hash,https://github.com/rust-lang/rustc-hash,Apache-2.0 OR MIT,The Rust Pr
173173
rustix,https://github.com/bytecodealliance/rustix,Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT,"Dan Gohman <[email protected]>, Jakub Konka <[email protected]>"
174174
rustls,https://github.com/rustls/rustls,Apache-2.0 OR ISC OR MIT,The rustls Authors
175175
rustls-native-certs,https://github.com/rustls/rustls-native-certs,Apache-2.0 OR ISC OR MIT,The rustls-native-certs Authors
176+
rustls-pemfile,https://github.com/rustls/pemfile,Apache-2.0 OR ISC OR MIT,The rustls-pemfile Authors
176177
rustls-pki-types,https://github.com/rustls/pki-types,MIT OR Apache-2.0,The rustls-pki-types Authors
177178
rustls-webpki,https://github.com/rustls/webpki,ISC,The rustls-webpki Authors
178179
ryu,https://github.com/dtolnay/ryu,Apache-2.0 OR BSL-1.0,David Tolnay <[email protected]>

bottlecap/src/config/env.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ pub struct EnvConfig {
7575
/// The transport type to use for sending logs. Possible values are "auto" or "http1".
7676
#[serde(deserialize_with = "deserialize_optional_string")]
7777
pub http_protocol: Option<String>,
78+
/// @env `DD_TLS_CERT_FILE`
79+
/// The path to a file of concatenated CA certificates in PEM format.
80+
/// Example: `/opt/ca-cert.pem`
81+
#[serde(deserialize_with = "deserialize_optional_string")]
82+
pub tls_cert_file: Option<String>,
7883

7984
// Metrics
8085
/// @env `DD_DD_URL`
@@ -466,6 +471,7 @@ fn merge_config(config: &mut Config, env_config: &EnvConfig) {
466471
merge_option!(config, env_config, proxy_https);
467472
merge_vec!(config, env_config, proxy_no_proxy);
468473
merge_option!(config, env_config, http_protocol);
474+
merge_option!(config, env_config, tls_cert_file);
469475

470476
// Endpoints
471477
merge_string!(config, env_config, dd_url);
@@ -695,6 +701,7 @@ mod tests {
695701
jail.set_env("DD_PROXY_HTTPS", "https://proxy.example.com");
696702
jail.set_env("DD_PROXY_NO_PROXY", "localhost,127.0.0.1");
697703
jail.set_env("DD_HTTP_PROTOCOL", "http1");
704+
jail.set_env("DD_TLS_CERT_FILE", "/opt/ca-cert.pem");
698705

699706
// Metrics
700707
jail.set_env("DD_DD_URL", "https://metrics.datadoghq.com");
@@ -850,6 +857,7 @@ mod tests {
850857
proxy_https: Some("https://proxy.example.com".to_string()),
851858
proxy_no_proxy: vec!["localhost".to_string(), "127.0.0.1".to_string()],
852859
http_protocol: Some("http1".to_string()),
860+
tls_cert_file: Some("/opt/ca-cert.pem".to_string()),
853861
dd_url: "https://metrics.datadoghq.com".to_string(),
854862
url: "https://app.datadoghq.com".to_string(),
855863
additional_endpoints: HashMap::from([

bottlecap/src/config/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ pub struct Config {
252252
pub proxy_https: Option<String>,
253253
pub proxy_no_proxy: Vec<String>,
254254
pub http_protocol: Option<String>,
255+
pub tls_cert_file: Option<String>,
255256

256257
// Endpoints
257258
pub dd_url: String,
@@ -366,6 +367,7 @@ impl Default for Config {
366367
proxy_https: None,
367368
proxy_no_proxy: vec![],
368369
http_protocol: None,
370+
tls_cert_file: None,
369371

370372
// Endpoints
371373
dd_url: String::default(),

bottlecap/src/config/yaml.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ pub struct YamlConfig {
5353
pub dd_url: Option<String>,
5454
#[serde(deserialize_with = "deserialize_optional_string")]
5555
pub http_protocol: Option<String>,
56+
#[serde(deserialize_with = "deserialize_optional_string")]
57+
pub tls_cert_file: Option<String>,
5658

5759
// Endpoints
5860
#[serde(deserialize_with = "deserialize_additional_endpoints")]
@@ -417,6 +419,7 @@ fn merge_config(config: &mut Config, yaml_config: &YamlConfig) {
417419
merge_option!(config, proxy_https, yaml_config.proxy, https);
418420
merge_option_to_value!(config, proxy_no_proxy, yaml_config.proxy, no_proxy);
419421
merge_option!(config, yaml_config, http_protocol);
422+
merge_option!(config, yaml_config, tls_cert_file);
420423

421424
// Endpoints
422425
merge_hashmap!(config, yaml_config, additional_endpoints);
@@ -747,6 +750,7 @@ proxy:
747750
no_proxy: ["localhost", "127.0.0.1"]
748751
dd_url: "https://metrics.datadoghq.com"
749752
http_protocol: "http1"
753+
tls_cert_file: "/opt/ca-cert.pem"
750754
751755
# Endpoints
752756
additional_endpoints:
@@ -882,6 +886,7 @@ api_security_sample_delay: 60 # Seconds
882886
proxy_https: Some("https://proxy.example.com".to_string()),
883887
proxy_no_proxy: vec!["localhost".to_string(), "127.0.0.1".to_string()],
884888
http_protocol: Some("http1".to_string()),
889+
tls_cert_file: Some("/opt/ca-cert.pem".to_string()),
885890
dd_url: "https://metrics.datadoghq.com".to_string(),
886891
url: String::new(), // doesnt exist in yaml
887892
additional_endpoints: HashMap::from([

bottlecap/src/traces/stats_flusher.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,10 @@ impl StatsFlusher for ServerlessStatsFlusher {
102102

103103
let start = std::time::Instant::now();
104104

105-
let Ok(http_client) =
106-
ServerlessTraceFlusher::get_http_client(self.config.proxy_https.as_ref())
107-
else {
105+
let Ok(http_client) = ServerlessTraceFlusher::get_http_client(
106+
self.config.proxy_https.as_ref(),
107+
self.config.tls_cert_file.as_ref(),
108+
) else {
108109
error!("STATS_FLUSHER | Failed to create HTTP client");
109110
return;
110111
};

bottlecap/src/traces/trace_flusher.rs

Lines changed: 86 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,21 @@
44
use async_trait::async_trait;
55
use dogstatsd::api_key::ApiKeyFactory;
66
use hyper_http_proxy;
7+
use hyper_rustls::HttpsConnectorBuilder;
78
use libdd_common::{Endpoint, GenericHttpClient, hyper_migration};
89
use libdd_trace_utils::{
910
config_utils::trace_intake_url_prefixed,
1011
send_data::SendDataBuilder,
1112
trace_utils::{self, SendData},
1213
};
14+
use rustls::RootCertStore;
15+
use rustls_pki_types::CertificateDer;
1316
use std::error::Error;
17+
use std::fs::File;
18+
use std::io::BufReader;
1419
use std::str::FromStr;
1520
use std::sync::Arc;
21+
use std::sync::LazyLock;
1622
use tokio::task::JoinSet;
1723
use tracing::{debug, error};
1824

@@ -35,6 +41,7 @@ pub trait TraceFlusher {
3541
traces: Vec<SendData>,
3642
endpoint: Option<&Endpoint>,
3743
proxy_https: &Option<String>,
44+
tls_cert_file: &Option<String>,
3845
) -> Option<Vec<SendData>>;
3946

4047
/// Flushes traces by getting every available batch on the aggregator.
@@ -104,7 +111,13 @@ impl TraceFlusher for ServerlessTraceFlusher {
104111
"TRACES | Retrying to send {} previously failed batches",
105112
traces.len()
106113
);
107-
let retry_result = Self::send(traces, None, &self.config.proxy_https).await;
114+
let retry_result = Self::send(
115+
traces,
116+
None,
117+
&self.config.proxy_https,
118+
&self.config.tls_cert_file,
119+
)
120+
.await;
108121
if retry_result.is_some() {
109122
// Still failed, return to retry later
110123
return retry_result;
@@ -131,13 +144,17 @@ impl TraceFlusher for ServerlessTraceFlusher {
131144

132145
let traces_clone = traces.clone();
133146
let proxy_https = self.config.proxy_https.clone();
134-
batch_tasks.spawn(async move { Self::send(traces_clone, None, &proxy_https).await });
147+
let tls_cert_file = self.config.tls_cert_file.clone();
148+
batch_tasks.spawn(async move {
149+
Self::send(traces_clone, None, &proxy_https, &tls_cert_file).await
150+
});
135151

136152
for endpoint in self.additional_endpoints.clone() {
137153
let traces_clone = traces.clone();
138154
let proxy_https = self.config.proxy_https.clone();
155+
let tls_cert_file = self.config.tls_cert_file.clone();
139156
batch_tasks.spawn(async move {
140-
Self::send(traces_clone, Some(&endpoint), &proxy_https).await
157+
Self::send(traces_clone, Some(&endpoint), &proxy_https, &tls_cert_file).await
141158
});
142159
}
143160
}
@@ -158,6 +175,7 @@ impl TraceFlusher for ServerlessTraceFlusher {
158175
traces: Vec<SendData>,
159176
endpoint: Option<&Endpoint>,
160177
proxy_https: &Option<String>,
178+
tls_cert_file: &Option<String>,
161179
) -> Option<Vec<SendData>> {
162180
if traces.is_empty() {
163181
return None;
@@ -167,7 +185,9 @@ impl TraceFlusher for ServerlessTraceFlusher {
167185
tokio::task::yield_now().await;
168186
debug!("TRACES | Flushing {} traces", coalesced_traces.len());
169187

170-
let Ok(http_client) = ServerlessTraceFlusher::get_http_client(proxy_https.as_ref()) else {
188+
let Ok(http_client) =
189+
ServerlessTraceFlusher::get_http_client(proxy_https.as_ref(), tls_cert_file.as_ref())
190+
else {
171191
error!("TRACES | Failed to create HTTP client");
172192
return None;
173193
};
@@ -192,25 +212,79 @@ impl TraceFlusher for ServerlessTraceFlusher {
192212
}
193213
}
194214

215+
// Initialize the crypto provider needed for setting custom root certificates
216+
fn ensure_crypto_provider_initialized() {
217+
static INIT_CRYPTO_PROVIDER: LazyLock<()> = LazyLock::new(|| {
218+
#[cfg(unix)]
219+
rustls::crypto::aws_lc_rs::default_provider()
220+
.install_default()
221+
.expect("Failed to install default CryptoProvider");
222+
});
223+
224+
let () = &*INIT_CRYPTO_PROVIDER;
225+
}
226+
195227
impl ServerlessTraceFlusher {
196228
pub fn get_http_client(
197229
proxy_https: Option<&String>,
230+
tls_cert_file: Option<&String>,
198231
) -> Result<
199232
GenericHttpClient<hyper_http_proxy::ProxyConnector<libdd_common::connector::Connector>>,
200233
Box<dyn Error>,
201234
> {
235+
// Create the base connector with optional custom TLS config
236+
let connector = if let Some(ca_cert_path) = tls_cert_file {
237+
// Ensure crypto provider is initialized before creating TLS config
238+
ensure_crypto_provider_initialized();
239+
240+
// Load the custom certificate
241+
let cert_file = File::open(ca_cert_path)?;
242+
let mut reader = BufReader::new(cert_file);
243+
let certs: Vec<CertificateDer> =
244+
rustls_pemfile::certs(&mut reader).collect::<Result<Vec<_>, _>>()?;
245+
246+
// Create a root certificate store and add custom certs
247+
let mut root_store = RootCertStore::empty();
248+
for cert in certs {
249+
root_store.add(cert)?;
250+
}
251+
252+
// Build the TLS config with custom root certificates
253+
let tls_config = rustls::ClientConfig::builder()
254+
.with_root_certificates(root_store)
255+
.with_no_client_auth();
256+
257+
// Build the HTTPS connector with custom config
258+
let https_connector = HttpsConnectorBuilder::new()
259+
.with_tls_config(tls_config)
260+
.https_or_http()
261+
.enable_http1()
262+
.build();
263+
264+
debug!(
265+
"TRACES | GET_HTTP_CLIENT | Added root certificate from {}",
266+
ca_cert_path
267+
);
268+
269+
// Construct the Connector::Https variant directly
270+
libdd_common::connector::Connector::Https(https_connector)
271+
} else {
272+
// Use default connector
273+
libdd_common::connector::Connector::default()
274+
};
275+
202276
if let Some(proxy) = proxy_https {
203277
let proxy =
204278
hyper_http_proxy::Proxy::new(hyper_http_proxy::Intercept::Https, proxy.parse()?);
205-
let proxy_connector = hyper_http_proxy::ProxyConnector::from_proxy(
206-
libdd_common::connector::Connector::default(),
207-
proxy,
208-
)?;
209-
Ok(hyper_migration::client_builder().build(proxy_connector))
279+
let proxy_connector = hyper_http_proxy::ProxyConnector::from_proxy(connector, proxy)?;
280+
let client = hyper_migration::client_builder().build(proxy_connector);
281+
debug!(
282+
"TRACES | GET_HTTP_CLIENT | Proxy connector created with proxy: {:?}",
283+
proxy_https
284+
);
285+
Ok(client)
210286
} else {
211-
let proxy_connector = hyper_http_proxy::ProxyConnector::new(
212-
libdd_common::connector::Connector::default(),
213-
)?;
287+
let proxy_connector = hyper_http_proxy::ProxyConnector::new(connector)?;
214288
Ok(hyper_migration::client_builder().build(proxy_connector))
215289
}
216290
}

0 commit comments

Comments
 (0)