Skip to content

Commit 3f33477

Browse files
feat: [SVLS-6242] FIPSish client builder for reqwest (#13)
Intended for use in the datadog-lambda-extension, for now. Other users should evaluate carefully. Co-authored-by: jordan gonzález <30836115+duncanista@users.noreply.github.com>
1 parent 18f89a5 commit 3f33477

9 files changed

Lines changed: 139 additions & 5 deletions

File tree

.github/workflows/cargo.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,14 @@ jobs:
5353
shell: bash
5454
run: chmod +x ./scripts/install-protoc.sh && ./scripts/install-protoc.sh $HOME
5555
- shell: bash
56-
run: cargo clippy --workspace --all-features
56+
run: |
57+
if [[ "${{ inputs.runner }}" == "windows-2022" ]]; then
58+
# we don't technially support the datadog-fips crate on windows
59+
# right now anyway, so let's set this so that the windows build
60+
# doesn't fail.
61+
export AWS_LC_FIPS_SYS_NO_ASM=1
62+
fi
63+
cargo clippy --workspace --all-features
5764
5865
build:
5966
name: Build

Cargo.lock

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

clippy.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
disallowed-methods = [
2+
{ path = "reqwest::Client::builder", reason = "prefer the FIPS-compatible adapter", replacement = "datadog_fips::reqwest_adapter::create_reqwest_client_builder" },
3+
]

crates/datadog-fips/Cargo.toml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[package]
2+
name = "datadog-fips"
3+
version = "0.1.0"
4+
edition.workspace = true
5+
license.workspace = true
6+
homepage.workspace = true
7+
repository.workspace = true
8+
9+
[dependencies]
10+
reqwest = { version = "0.12.4", features = ["json", "http2"], default-features = false }
11+
rustls = { version = "0.23.18", default-features = false, features = ["fips"], optional = true }
12+
rustls-native-certs = { version = "0.8.1", optional = true }
13+
tracing = { version = "0.1.40", default-features = false }
14+
15+
[features]
16+
default = [ "reqwest/rustls-tls" ]
17+
fips = [ "reqwest/rustls-tls-no-provider", "rustls", "rustls-native-certs" ]

crates/datadog-fips/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Datadog FIPS for Serverless
2+
3+
Crate which provides utils to build FIPS compliant components.
4+
5+
Please add the following to your `clippy.toml`:
6+
7+
```
8+
disallowed-methods = [
9+
{ path = "reqwest::Client::builder", reason = "prefer the FIPS-compatible adapter", replacement = "datadog_fips::reqwest_adapter::create_reqwest_client_builder" },
10+
]
11+
```

crates/datadog-fips/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
pub mod reqwest_adapter;
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
use reqwest::ClientBuilder;
2+
use std::error::Error;
3+
#[cfg(feature = "fips")]
4+
use tracing::debug;
5+
6+
/// Creates a reqwest client builder with TLS configuration.
7+
/// When the "fips" feature is enabled, it uses a FIPS-compliant TLS configuration.
8+
/// Otherwise, it uses reqwest's default rustls TLS implementation.
9+
#[cfg(not(feature = "fips"))]
10+
pub fn create_reqwest_client_builder() -> Result<ClientBuilder, Box<dyn Error>> {
11+
// Just return the default builder with rustls TLS. This is the one place we should be okay
12+
// to call reqwest::Client::builder().
13+
#[allow(clippy::disallowed_methods)]
14+
Ok(reqwest::Client::builder().use_rustls_tls())
15+
}
16+
17+
/// Creates a reqwest client builder with FIPS-compliant TLS configuration.
18+
/// This version loads native root certificates and verifies FIPS compliance.
19+
#[cfg(feature = "fips")]
20+
pub fn create_reqwest_client_builder() -> Result<ClientBuilder, Box<dyn Error>> {
21+
// Get the runtime crypto provider that should have been configured at the start of the
22+
// application using something like rustls::crypto::default_fips_provider().install_default()
23+
let provider =
24+
rustls::crypto::CryptoProvider::get_default().ok_or("No crypto provider configured")?;
25+
26+
if !provider.fips() {
27+
return Err("Crypto provider is not FIPS-compliant".into());
28+
}
29+
30+
let mut root_cert_store = rustls::RootCertStore::empty();
31+
let native_certs = rustls_native_certs::load_native_certs();
32+
let mut valid_count = 0;
33+
for cert in native_certs.certs {
34+
match root_cert_store.add(cert) {
35+
Ok(()) => valid_count += 1,
36+
Err(err) => {
37+
debug!("Failed to parse certificate: {:?}", err);
38+
}
39+
}
40+
}
41+
if valid_count == 0 {
42+
return Err("No valid certificates found in native root store".into());
43+
}
44+
45+
// FIPS typically requires TLS 1.2 or higher
46+
let versions = rustls::ALL_VERSIONS.to_vec();
47+
let config_builder = rustls::ClientConfig::builder_with_provider(provider.clone())
48+
.with_protocol_versions(&versions)
49+
.map_err(|_| "Failed to set protocol versions")?;
50+
51+
let config = config_builder
52+
.with_root_certificates(root_cert_store)
53+
.with_no_client_auth();
54+
55+
if !config.fips() {
56+
return Err("The final TLS configuration is not FIPS-compliant".into());
57+
}
58+
debug!("Client builder is configured with FIPS.");
59+
60+
// This is the one place that it is okay to call reqwest::Client::builder().
61+
#[allow(clippy::disallowed_methods)]
62+
Ok(reqwest::Client::builder().use_preconfigured_tls(config))
63+
}

crates/dogstatsd/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ tokio-util = { version = "0.7.11", default-features = false }
2424
tracing = { version = "0.1.40", default-features = false }
2525
regex = { version = "1.10.6", default-features = false }
2626
zstd = { version = "0.13.3", default-features = false }
27+
datadog-fips = { path = "../datadog-fips", default-features = false }
2728

2829
[dev-dependencies]
2930
mockito = { version = "1.5.0", default-features = false }
@@ -32,4 +33,4 @@ tracing-test = { version = "0.2.5", default-features = false }
3233

3334
[features]
3435
default = [ "reqwest/rustls-tls" ]
35-
fips = [ "reqwest/rustls-tls-no-provider" ]
36+
fips = [ "reqwest/rustls-tls-no-provider", "datadog-fips/fips" ]

crates/dogstatsd/src/datadog.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
//!Types to serialize data into the Datadog API
55
66
use crate::flusher::ShippingError;
7+
use datadog_fips::reqwest_adapter::create_reqwest_client_builder;
78
use datadog_protos::metrics::SketchPayload;
89
use derive_more::{Display, Into};
910
use protobuf::Message;
@@ -12,6 +13,7 @@ use reqwest;
1213
use reqwest::{Client, Response};
1314
use serde::{Serialize, Serializer};
1415
use serde_json;
16+
use std::error::Error;
1517
use std::io::Write;
1618
use std::sync::OnceLock;
1719
use std::time::Duration;
@@ -285,12 +287,12 @@ pub enum RetryStrategy {
285287
LinearBackoff(u64, u64), // attempts, delay
286288
}
287289

288-
fn build_client(https_proxy: Option<String>, timeout: Duration) -> Result<Client, reqwest::Error> {
289-
let mut builder = Client::builder().timeout(timeout);
290+
fn build_client(https_proxy: Option<String>, timeout: Duration) -> Result<Client, Box<dyn Error>> {
291+
let mut builder = create_reqwest_client_builder()?.timeout(timeout);
290292
if let Some(proxy) = https_proxy {
291293
builder = builder.proxy(reqwest::Proxy::https(proxy)?);
292294
}
293-
builder.build()
295+
Ok(builder.build()?)
294296
}
295297

296298
#[derive(Debug, Serialize, Clone, Copy)]

0 commit comments

Comments
 (0)