Skip to content

Commit cd9d1bb

Browse files
authored
feat(http): allow skip ssl validation (#1064)
## Overview Add DD_SKIP_SSL_VALIDATION support, parsed from both env and YAML, matching the datadog-agent's behavior — applied to all outgoing HTTP clients (reqwest via danger_accept_invalid_certs, hyper via custom ServerCertVerifier). ## Motivation Customers in environments with corporate proxies or custom CA setups need the ability to disable TLS certificate validation, matching the existing datadog-agent config option. The Go agent applies tls.Config{InsecureSkipVerify: true} to all HTTP transports via a central CreateHTTPTransport() — we mirror this by wiring the config through to both client builders. And [SLES-2710](https://datadoghq.atlassian.net/browse/SLES-2710) ## Changes Config (config/mod.rs, config/env.rs, config/yaml.rs): - Add skip_ssl_validation: bool to Config, EnvConfig, and YamlConfig with default false reqwest client (http.rs): - .danger_accept_invalid_certs(config.skip_ssl_validation) on the shared client builder hyper client (traces/http_client.rs): - Custom NoVerifier implementing rustls::client::danger::ServerCertVerifier that accepts all certificates - Uses CryptoProvider::get_default() (not hardcoded aws_lc_rs) for FIPS-safe signature scheme reporting - New skip_ssl_validation parameter on create_client() ## Testing Unit tests and self monitoring [SLES-2710]: https://datadoghq.atlassian.net/browse/SLES-2710?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
1 parent 3498b89 commit cd9d1bb

File tree

6 files changed

+106
-6
lines changed

6 files changed

+106
-6
lines changed

bottlecap/src/bin/bottlecap/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1094,6 +1094,7 @@ fn start_trace_agent(
10941094
let trace_http_client = trace_http_client::create_client(
10951095
config.proxy_https.as_ref(),
10961096
config.tls_cert_file.as_ref(),
1097+
config.skip_ssl_validation,
10971098
)
10981099
.expect("Failed to create trace HTTP client");
10991100

bottlecap/src/config/env.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ pub struct EnvConfig {
8080
/// Example: `/opt/ca-cert.pem`
8181
#[serde(deserialize_with = "deserialize_optional_string")]
8282
pub tls_cert_file: Option<String>,
83+
/// @env `DD_SKIP_SSL_VALIDATION`
84+
///
85+
/// If set to true, the Agent will skip TLS certificate validation for outgoing connections.
86+
#[serde(deserialize_with = "deserialize_optional_bool_from_anything")]
87+
pub skip_ssl_validation: Option<bool>,
8388

8489
// Metrics
8590
/// @env `DD_DD_URL`
@@ -497,6 +502,7 @@ fn merge_config(config: &mut Config, env_config: &EnvConfig) {
497502
merge_vec!(config, env_config, proxy_no_proxy);
498503
merge_option!(config, env_config, http_protocol);
499504
merge_option!(config, env_config, tls_cert_file);
505+
merge_option_to_value!(config, env_config, skip_ssl_validation);
500506

501507
// Endpoints
502508
merge_string!(config, env_config, dd_url);
@@ -733,6 +739,7 @@ mod tests {
733739
jail.set_env("DD_PROXY_NO_PROXY", "localhost,127.0.0.1");
734740
jail.set_env("DD_HTTP_PROTOCOL", "http1");
735741
jail.set_env("DD_TLS_CERT_FILE", "/opt/ca-cert.pem");
742+
jail.set_env("DD_SKIP_SSL_VALIDATION", "true");
736743

737744
// Metrics
738745
jail.set_env("DD_DD_URL", "https://metrics.datadoghq.com");
@@ -895,6 +902,7 @@ mod tests {
895902
proxy_no_proxy: vec!["localhost".to_string(), "127.0.0.1".to_string()],
896903
http_protocol: Some("http1".to_string()),
897904
tls_cert_file: Some("/opt/ca-cert.pem".to_string()),
905+
skip_ssl_validation: true,
898906
dd_url: "https://metrics.datadoghq.com".to_string(),
899907
url: "https://app.datadoghq.com".to_string(),
900908
additional_endpoints: HashMap::from([

bottlecap/src/config/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ pub struct Config {
256256
pub proxy_no_proxy: Vec<String>,
257257
pub http_protocol: Option<String>,
258258
pub tls_cert_file: Option<String>,
259+
pub skip_ssl_validation: bool,
259260

260261
// Endpoints
261262
pub dd_url: String,
@@ -383,6 +384,7 @@ impl Default for Config {
383384
proxy_no_proxy: vec![],
384385
http_protocol: None,
385386
tls_cert_file: None,
387+
skip_ssl_validation: false,
386388

387389
// Endpoints
388390
dd_url: String::default(),

bottlecap/src/config/yaml.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ pub struct YamlConfig {
5555
pub http_protocol: Option<String>,
5656
#[serde(deserialize_with = "deserialize_optional_string")]
5757
pub tls_cert_file: Option<String>,
58+
#[serde(deserialize_with = "deserialize_optional_bool_from_anything")]
59+
pub skip_ssl_validation: Option<bool>,
5860

5961
// Endpoints
6062
#[serde(deserialize_with = "deserialize_additional_endpoints")]
@@ -431,6 +433,7 @@ fn merge_config(config: &mut Config, yaml_config: &YamlConfig) {
431433
merge_option_to_value!(config, proxy_no_proxy, yaml_config.proxy, no_proxy);
432434
merge_option!(config, yaml_config, http_protocol);
433435
merge_option!(config, yaml_config, tls_cert_file);
436+
merge_option_to_value!(config, yaml_config, skip_ssl_validation);
434437

435438
// Endpoints
436439
merge_hashmap!(config, yaml_config, additional_endpoints);
@@ -767,6 +770,7 @@ proxy:
767770
dd_url: "https://metrics.datadoghq.com"
768771
http_protocol: "http1"
769772
tls_cert_file: "/opt/ca-cert.pem"
773+
skip_ssl_validation: true
770774
771775
# Endpoints
772776
additional_endpoints:
@@ -907,6 +911,7 @@ api_security_sample_delay: 60 # Seconds
907911
proxy_no_proxy: vec!["localhost".to_string(), "127.0.0.1".to_string()],
908912
http_protocol: Some("http1".to_string()),
909913
tls_cert_file: Some("/opt/ca-cert.pem".to_string()),
914+
skip_ssl_validation: true,
910915
dd_url: "https://metrics.datadoghq.com".to_string(),
911916
url: String::new(), // doesnt exist in yaml
912917
additional_endpoints: HashMap::from([

bottlecap/src/http.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ fn build_client(config: &Arc<config::Config>) -> Result<reqwest::Client, Box<dyn
2828
.timeout(Duration::from_secs(config.flush_timeout))
2929
.pool_idle_timeout(Some(Duration::from_secs(270)))
3030
// Enable TCP keepalive
31-
.tcp_keepalive(Some(Duration::from_secs(120)));
31+
.tcp_keepalive(Some(Duration::from_secs(120)))
32+
.danger_accept_invalid_certs(config.skip_ssl_validation);
3233

3334
// Determine if we should use HTTP/1 or HTTP/2
3435
let should_use_http1 = match &config.http_protocol {
@@ -47,6 +48,13 @@ fn build_client(config: &Arc<config::Config>) -> Result<reqwest::Client, Box<dyn
4748
.http2_keep_alive_timeout(Duration::from_secs(1000));
4849
}
4950

51+
if config.skip_ssl_validation && config.tls_cert_file.is_some() {
52+
debug!(
53+
"HTTP | skip_ssl_validation=true overrides tls_cert_file={:?}, custom certificate will be ignored",
54+
config.tls_cert_file
55+
);
56+
}
57+
5058
// Load custom TLS certificate if configured
5159
if let Some(cert_path) = &config.tls_cert_file {
5260
match load_custom_cert(cert_path) {

bottlecap/src/traces/http_client.rs

Lines changed: 81 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use rustls_pki_types::CertificateDer;
1414
use std::error::Error;
1515
use std::fs::File;
1616
use std::io::BufReader;
17-
use std::sync::LazyLock;
17+
use std::sync::{Arc, LazyLock};
1818
use tracing::debug;
1919

2020
/// Type alias for the HTTP client used by trace and stats flushers.
@@ -27,24 +27,75 @@ pub type HttpClient =
2727
fn ensure_crypto_provider_initialized() {
2828
static INIT_CRYPTO_PROVIDER: LazyLock<()> = LazyLock::new(|| {
2929
#[cfg(unix)]
30-
rustls::crypto::aws_lc_rs::default_provider()
31-
.install_default()
32-
.expect("Failed to install default CryptoProvider");
30+
if let Err(_already_installed) =
31+
rustls::crypto::aws_lc_rs::default_provider().install_default()
32+
{
33+
debug!(
34+
"HTTP_CLIENT | Default CryptoProvider already installed, using existing provider"
35+
);
36+
}
3337
});
3438

3539
let () = &*INIT_CRYPTO_PROVIDER;
3640
}
3741

42+
/// A certificate verifier that accepts all server certificates without validation.
43+
///
44+
/// This is used when `DD_SKIP_SSL_VALIDATION` is set to `true`.
45+
#[derive(Debug)]
46+
struct NoVerifier;
47+
48+
impl rustls::client::danger::ServerCertVerifier for NoVerifier {
49+
fn verify_server_cert(
50+
&self,
51+
_end_entity: &CertificateDer<'_>,
52+
_intermediates: &[CertificateDer<'_>],
53+
_server_name: &rustls_pki_types::ServerName<'_>,
54+
_ocsp_response: &[u8],
55+
_now: rustls_pki_types::UnixTime,
56+
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
57+
Ok(rustls::client::danger::ServerCertVerified::assertion())
58+
}
59+
60+
fn verify_tls12_signature(
61+
&self,
62+
_message: &[u8],
63+
_cert: &CertificateDer<'_>,
64+
_dss: &rustls::DigitallySignedStruct,
65+
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
66+
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
67+
}
68+
69+
fn verify_tls13_signature(
70+
&self,
71+
_message: &[u8],
72+
_cert: &CertificateDer<'_>,
73+
_dss: &rustls::DigitallySignedStruct,
74+
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
75+
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
76+
}
77+
78+
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
79+
// Safe to unwrap_or_default: the provider is always initialized via
80+
// ensure_crypto_provider_initialized() before NoVerifier is used.
81+
rustls::crypto::CryptoProvider::get_default()
82+
.map(|p| p.signature_verification_algorithms.supported_schemes())
83+
.unwrap_or_default()
84+
}
85+
}
86+
3887
/// Creates a new HTTP client with the given configuration.
3988
///
4089
/// This client is compatible with `libdd_trace_utils` and supports:
4190
/// - HTTPS proxy configuration
4291
/// - Custom TLS root certificates
92+
/// - Skipping TLS certificate validation
4393
///
4494
/// # Arguments
4595
///
4696
/// * `proxy_https` - Optional HTTPS proxy URL
4797
/// * `tls_cert_file` - Optional path to a PEM file containing root certificates
98+
/// * `skip_ssl_validation` - If true, skip TLS certificate validation
4899
///
49100
/// # Errors
50101
///
@@ -54,9 +105,34 @@ fn ensure_crypto_provider_initialized() {
54105
pub fn create_client(
55106
proxy_https: Option<&String>,
56107
tls_cert_file: Option<&String>,
108+
skip_ssl_validation: bool,
57109
) -> Result<HttpClient, Box<dyn Error>> {
110+
if skip_ssl_validation && tls_cert_file.is_some() {
111+
debug!(
112+
"HTTP_CLIENT | skip_ssl_validation=true overrides tls_cert_file={:?}, custom certificate will be ignored",
113+
tls_cert_file
114+
);
115+
}
116+
58117
// Create the base connector with optional custom TLS config
59-
let connector = if let Some(ca_cert_path) = tls_cert_file {
118+
let connector = if skip_ssl_validation {
119+
ensure_crypto_provider_initialized();
120+
121+
let tls_config = rustls::ClientConfig::builder()
122+
.dangerous()
123+
.with_custom_certificate_verifier(Arc::new(NoVerifier))
124+
.with_no_client_auth();
125+
126+
let https_connector = HttpsConnectorBuilder::new()
127+
.with_tls_config(tls_config)
128+
.https_or_http()
129+
.enable_http1()
130+
.build();
131+
132+
debug!("HTTP_CLIENT | TLS certificate validation disabled (skip_ssl_validation=true)");
133+
134+
libdd_common::connector::Connector::Https(https_connector)
135+
} else if let Some(ca_cert_path) = tls_cert_file {
60136
// Ensure crypto provider is initialized before creating TLS config
61137
ensure_crypto_provider_initialized();
62138

0 commit comments

Comments
 (0)