Skip to content

Commit 56fed4d

Browse files
authored
Turbopack: Add an option to use system TLS certificates (fixes vercel#79060, fixes vercel#79059) (vercel#81818)
It's common in enterprise environments for employers to MITM all HTTPS traffic on employee machines to enforce network policies or to detect and block malware. For this to work, they install custom CA roots into the system store. Applications must read from this store. The default behavior of `reqwests` is to bundle the mozilla CA roots into the application, and only trust those. This is reasonable given some of the tradeoffs of the current rustls resolver implementations they use, but long-term it's better for most applications to just use the system CA store. This provides an opt-in experimental option for using the system CA store. We may use system CA certs by default in the future once seanmonstar/reqwest#2159 is resolved. Fixes vercel#79059 Fixes vercel#79060 ### Testing - Install [mitmproxy](https://docs.mitmproxy.org/stable/overview/installation/). - Install the generated mitmproxy CA cert to the system store: https://docs.mitmproxy.org/stable/concepts/certificates/ - Run `./mitmdump` (with no arguments) - Run an example app that uses google fonts: ``` pnpm pack-next --project ~/shadcn-ui/apps/v4 --no-js-build -- --release cd ~/shadcn-ui/apps/v4 pnpm i NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS=0 HTTP_PROXY=localhost:8080 HTTPS_PROXY=localhost:8080 pnpm dev NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS=1 HTTP_PROXY=localhost:8080 HTTPS_PROXY=localhost:8080 pnpm dev ``` When system TLS certs are disabled, we get warnings like this: ``` ⚠ [next]/internal/font/google/geist_mono_77f2790.module.css Error while requesting resource There was an issue establishing a connection while requesting https://fonts.googleapis.com/css2?family=Geist+Mono:wght@400&display=swap. Import trace: [next]/internal/font/google/geist_mono_77f2790.module.css [next]/internal/font/google/geist_mono_77f2790.js ./apps/v4/lib/fonts.ts ./apps/v4/app/layout.tsx ``` And mitmproxy shows the handshakes failing ![Screenshot 2025-07-21 at 1.11.11 PM.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/HAZVitxRNnZz8QMiPn4a/2efc4b97-f50e-412c-9daf-5cdd00a6d82b.png) When system TLS certs are enabled, the app works.
1 parent 7a183a0 commit 56fed4d

File tree

10 files changed

+180
-28
lines changed

10 files changed

+180
-28
lines changed

.changeset/silent-houses-lay.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@next/swc': patch
3+
---
4+
5+
Added an experimental option for using the system CA store for fetching Google Fonts in Turbopack

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ rand = "0.9.0"
400400
rayon = "1.10.0"
401401
regex = "1.10.6"
402402
regress = "0.10.3"
403-
reqwest = { version = "0.12.20", default-features = false }
403+
reqwest = { version = "0.12.22", default-features = false }
404404
ringmap = "0.1.3"
405405
roaring = "0.10.10"
406406
rstest = "0.16.0"

crates/next-core/src/next_config.rs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ use rustc_hash::FxHashSet;
33
use serde::{Deserialize, Deserializer, Serialize};
44
use serde_json::Value as JsonValue;
55
use turbo_esregex::EsRegex;
6-
use turbo_rcstr::RcStr;
6+
use turbo_rcstr::{RcStr, rcstr};
77
use turbo_tasks::{
88
FxIndexMap, NonLocalValue, OperationValue, ResolvedVc, TaskInput, Vc, debug::ValueDebugFormat,
99
trace::TraceRawVcs,
1010
};
11-
use turbo_tasks_env::EnvMap;
11+
use turbo_tasks_env::{EnvMap, ProcessEnv};
12+
use turbo_tasks_fetch::ReqwestClientConfig;
1213
use turbo_tasks_fs::FileSystemPath;
1314
use turbopack::module_options::{
1415
ConditionItem, ConditionPath, LoaderRuleItem, OptionWebpackRules,
@@ -801,6 +802,7 @@ pub struct ExperimentalConfig {
801802
turbopack_source_maps: Option<bool>,
802803
turbopack_tree_shaking: Option<bool>,
803804
turbopack_scope_hoisting: Option<bool>,
805+
turbopack_use_system_tls_certs: Option<bool>,
804806
// Whether to enable the global-not-found convention
805807
global_not_found: Option<bool>,
806808
/// Defaults to false in development mode, true in production mode.
@@ -1721,6 +1723,32 @@ impl NextConfig {
17211723
pub fn output_file_tracing_excludes(&self) -> Vc<OptionJsonValue> {
17221724
Vc::cell(self.output_file_tracing_excludes.clone())
17231725
}
1726+
1727+
#[turbo_tasks::function]
1728+
pub async fn reqwest_client_config(
1729+
&self,
1730+
env: Vc<Box<dyn ProcessEnv>>,
1731+
) -> Result<Vc<ReqwestClientConfig>> {
1732+
// Support both an env var and the experimental flag to provide more flexibility to
1733+
// developers on locked down systems, depending on if they want to configure this on a
1734+
// per-system or per-project basis.
1735+
let use_system_tls_certs = env
1736+
.read(rcstr!("NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS"))
1737+
.await?
1738+
.as_ref()
1739+
.and_then(|env_value| {
1740+
// treat empty value same as an unset value
1741+
(!env_value.is_empty()).then(|| env_value == "1" || env_value == "true")
1742+
})
1743+
.or(self.experimental.turbopack_use_system_tls_certs)
1744+
.unwrap_or(false);
1745+
Ok(ReqwestClientConfig {
1746+
proxy: None,
1747+
tls_built_in_webpki_certs: !use_system_tls_certs,
1748+
tls_built_in_native_certs: use_system_tls_certs,
1749+
}
1750+
.cell())
1751+
}
17241752
}
17251753

17261754
/// A subset of ts/jsconfig that next.js implicitly

crates/next-core/src/next_font/google/mod.rs

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ pub struct NextFontGoogleCssModuleReplacer {
182182
project_path: FileSystemPath,
183183
execution_context: ResolvedVc<ExecutionContext>,
184184
next_mode: ResolvedVc<NextMode>,
185+
reqwest_client_config: ResolvedVc<ReqwestClientConfig>,
185186
}
186187

187188
#[turbo_tasks::value_impl]
@@ -191,11 +192,13 @@ impl NextFontGoogleCssModuleReplacer {
191192
project_path: FileSystemPath,
192193
execution_context: ResolvedVc<ExecutionContext>,
193194
next_mode: ResolvedVc<NextMode>,
195+
reqwest_client_config: ResolvedVc<ReqwestClientConfig>,
194196
) -> Vc<Self> {
195197
Self::cell(NextFontGoogleCssModuleReplacer {
196198
project_path,
197199
execution_context,
198200
next_mode,
201+
reqwest_client_config,
199202
})
200203
}
201204

@@ -228,7 +231,14 @@ impl NextFontGoogleCssModuleReplacer {
228231
let stylesheet_str = mocked_responses_path
229232
.as_ref()
230233
.map_or_else(
231-
|| fetch_real_stylesheet(stylesheet_url.clone(), css_virtual_path.clone()).boxed(),
234+
|| {
235+
fetch_real_stylesheet(
236+
stylesheet_url.clone(),
237+
css_virtual_path.clone(),
238+
*self.reqwest_client_config,
239+
)
240+
.boxed()
241+
},
232242
|p| get_mock_stylesheet(stylesheet_url.clone(), p, *self.execution_context).boxed(),
233243
)
234244
.await?;
@@ -365,13 +375,20 @@ struct NextFontGoogleFontFileOptions {
365375
#[turbo_tasks::value(shared)]
366376
pub struct NextFontGoogleFontFileReplacer {
367377
project_path: FileSystemPath,
378+
reqwest_client_config: ResolvedVc<ReqwestClientConfig>,
368379
}
369380

370381
#[turbo_tasks::value_impl]
371382
impl NextFontGoogleFontFileReplacer {
372383
#[turbo_tasks::function]
373-
pub fn new(project_path: FileSystemPath) -> Vc<Self> {
374-
Self::cell(NextFontGoogleFontFileReplacer { project_path })
384+
pub fn new(
385+
project_path: FileSystemPath,
386+
reqwest_client_config: ResolvedVc<ReqwestClientConfig>,
387+
) -> Vc<Self> {
388+
Self::cell(NextFontGoogleFontFileReplacer {
389+
project_path,
390+
reqwest_client_config,
391+
})
375392
}
376393
}
377394

@@ -426,7 +443,12 @@ impl ImportMappingReplacement for NextFontGoogleFontFileReplacer {
426443

427444
// doesn't seem ideal to download the font into a string, but probably doesn't
428445
// really matter either.
429-
let Some(font) = fetch_from_google_fonts(url.into(), font_virtual_path.clone()).await?
446+
let Some(font) = fetch_from_google_fonts(
447+
url.into(),
448+
font_virtual_path.clone(),
449+
*self.reqwest_client_config,
450+
)
451+
.await?
430452
else {
431453
return Ok(ImportMapResult::Result(ResolveResult::unresolvable()).cell());
432454
};
@@ -650,20 +672,23 @@ fn font_file_options_from_query_map(query: &RcStr) -> Result<NextFontGoogleFontF
650672
async fn fetch_real_stylesheet(
651673
stylesheet_url: RcStr,
652674
css_virtual_path: FileSystemPath,
675+
reqwest_client_config: Vc<ReqwestClientConfig>,
653676
) -> Result<Option<Vc<RcStr>>> {
654-
let body = fetch_from_google_fonts(stylesheet_url, css_virtual_path).await?;
677+
let body =
678+
fetch_from_google_fonts(stylesheet_url, css_virtual_path, reqwest_client_config).await?;
655679

656680
Ok(body.map(|body| body.to_string()))
657681
}
658682

659683
async fn fetch_from_google_fonts(
660684
url: RcStr,
661685
virtual_path: FileSystemPath,
686+
reqwest_client_config: Vc<ReqwestClientConfig>,
662687
) -> Result<Option<Vc<HttpResponseBody>>> {
663688
let result = fetch(
664689
url,
665690
Some(rcstr!(USER_AGENT_FOR_GOOGLE_FONTS)),
666-
ReqwestClientConfig { proxy: None }.cell(),
691+
reqwest_client_config,
667692
)
668693
.await?;
669694

crates/next-core/src/next_import_map.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1022,13 +1022,15 @@ async fn insert_next_shared_aliases(
10221022
next_font_google_replacer_mapping,
10231023
);
10241024

1025+
let reqwest_client_config = next_config.reqwest_client_config(execution_context.env());
10251026
import_map.insert_alias(
10261027
AliasPattern::exact("@vercel/turbopack-next/internal/font/google/cssmodule.module.css"),
10271028
ImportMapping::Dynamic(ResolvedVc::upcast(
10281029
NextFontGoogleCssModuleReplacer::new(
10291030
project_path.clone(),
10301031
execution_context,
10311032
next_mode,
1033+
reqwest_client_config,
10321034
)
10331035
.to_resolved()
10341036
.await?,
@@ -1039,7 +1041,7 @@ async fn insert_next_shared_aliases(
10391041
import_map.insert_alias(
10401042
AliasPattern::exact(GOOGLE_FONTS_INTERNAL_PREFIX),
10411043
ImportMapping::Dynamic(ResolvedVc::upcast(
1042-
NextFontGoogleFontFileReplacer::new(project_path.clone())
1044+
NextFontGoogleFontFileReplacer::new(project_path.clone(), reqwest_client_config)
10431045
.to_resolved()
10441046
.await?,
10451047
))

packages/next/src/server/config-schema.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,26 @@ export const configSchema: zod.ZodType<NextConfig> = z.lazy(() =>
462462
turbopackTreeShaking: z.boolean().optional(),
463463
turbopackRemoveUnusedExports: z.boolean().optional(),
464464
turbopackScopeHoisting: z.boolean().optional(),
465+
/**
466+
* Use the system-provided CA roots instead of bundled CA roots for external HTTPS requests
467+
* made by Turbopack. Currently this is only used for fetching data from Google Fonts.
468+
*
469+
* This may be useful in cases where you or an employer are MITMing traffic.
470+
*
471+
* This option is experimental because:
472+
* - This may cause small performance problems, as it uses [`rustls-native-certs`](
473+
* https://github.com/rustls/rustls-native-certs).
474+
* - In the future, this may become the default, and this option may be eliminated, once
475+
* <https://github.com/seanmonstar/reqwest/issues/2159> is resolved.
476+
*
477+
* Users who need to configure this behavior system-wide can override the project
478+
* configuration using the `NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS=1` environment
479+
* variable.
480+
*
481+
* This option is ignored on Windows on ARM, where the native TLS implementation is always
482+
* used.
483+
*/
484+
turbopackUseSystemTlsCerts: z.boolean().optional(),
465485
optimizePackageImports: z.array(z.string()).optional(),
466486
optimizeServerReact: z.boolean().optional(),
467487
clientTraceMetadata: z.array(z.string()).optional(),

turbopack/crates/turbo-tasks-fetch/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ workspace = true
2020
[target.'cfg(all(target_os = "windows", target_arch = "aarch64"))'.dependencies]
2121
reqwest = { workspace = true, features = ["native-tls"] }
2222

23+
# If you modify this cfg, make sure you update `ReqwestClientConfig::try_build`.
2324
[target.'cfg(not(any(all(target_os = "windows", target_arch = "aarch64"), target_arch="wasm32")))'.dependencies]
24-
reqwest = { workspace = true, features = ["rustls-tls"] }
25+
reqwest = { workspace = true, features = ["rustls-tls-webpki-roots", "rustls-tls-native-roots"] }
2526

2627
[dependencies]
2728
anyhow = { workspace = true }

turbopack/crates/turbo-tasks-fetch/src/reqwest_client_cache.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,29 @@ pub enum ProxyConfig {
2626
#[derive(Hash)]
2727
pub struct ReqwestClientConfig {
2828
pub proxy: Option<ProxyConfig>,
29+
/// Whether to load embedded webpki root certs with rustls. Default is true.
30+
///
31+
/// Ignored for:
32+
/// - Windows on ARM, which uses `native-tls` instead of `rustls-tls`.
33+
/// - Ignored for WASM targets, which use the runtime's TLS implementation.
34+
pub tls_built_in_webpki_certs: bool,
35+
/// Whether to load native root certs using the `rustls-native-certs` crate. This may make
36+
/// reqwest client initialization slower, so it's not used by default.
37+
///
38+
/// Ignored for:
39+
/// - Windows on ARM, which uses `native-tls` instead of `rustls-tls`.
40+
/// - Ignored for WASM targets, which use the runtime's TLS implementation.
41+
pub tls_built_in_native_certs: bool,
42+
}
43+
44+
impl Default for ReqwestClientConfig {
45+
fn default() -> Self {
46+
Self {
47+
proxy: None,
48+
tls_built_in_webpki_certs: true,
49+
tls_built_in_native_certs: false,
50+
}
51+
}
2952
}
3053

3154
impl ReqwestClientConfig {
@@ -40,6 +63,17 @@ impl ReqwestClientConfig {
4063
}
4164
None => {}
4265
};
66+
67+
// make sure this cfg matches the one in `Cargo.toml`!
68+
#[cfg(not(any(
69+
all(target_os = "windows", target_arch = "aarch64"),
70+
target_arch = "wasm32"
71+
)))]
72+
let client_builder = client_builder
73+
.tls_built_in_root_certs(false)
74+
.tls_built_in_webpki_certs(self.tls_built_in_webpki_certs)
75+
.tls_built_in_native_certs(self.tls_built_in_native_certs);
76+
4377
client_builder.build()
4478
}
4579
}

0 commit comments

Comments
 (0)