From d86075fc1ee84a67a22288a837988b00dd98431f Mon Sep 17 00:00:00 2001
From: Charlie Marsh
Date: Tue, 27 Aug 2024 09:36:50 -0400
Subject: [PATCH] Add support for `--trusted-host` (#6591)
## Summary
This PR revives https://github.com/astral-sh/uv/pull/4944, which I think
was a good start towards adding `--trusted-host`. Last night, I tried to
add `--trusted-host` with a custom verifier, but we had to vendor a lot
of `reqwest` code and I eventually hit some private APIs. I'm not
confident that I can implement it correctly with that mechanism, and
since this is security, correctness is the priority.
So, instead, we now use two clients and multiplex between them.
Closes https://github.com/astral-sh/uv/issues/1339.
## Test Plan
Created self-signed certificate, and ran `python3 -m http.server --bind
127.0.0.1 4443 --directory . --certfile cert.pem --keyfile key.pem` from
the packse index directory.
Verified that `cargo run pip install
transitive-yanked-and-unyanked-dependency-a-0abad3b6 --index-url
https://127.0.0.1:8443/simple-html` failed with:
```
error: Request failed after 3 retries
Caused by: error sending request for url (https://127.0.0.1:8443/simple-html/transitive-yanked-and-unyanked-dependency-a-0abad3b6/)
Caused by: client error (Connect)
Caused by: invalid peer certificate: Other(OtherError(CaUsedAsEndEntity))
```
Verified that `cargo run pip install
transitive-yanked-and-unyanked-dependency-a-0abad3b6 --index-url
'https://127.0.0.1:8443/simple-html' --trusted-host '127.0.0.1:8443'`
failed with the expected error (invalid resolution) and made valid
requests.
Verified that `cargo run pip install
transitive-yanked-and-unyanked-dependency-a-0abad3b6 --index-url
'https://127.0.0.1:8443/simple-html' --trusted-host '127.0.0.2' -n` also
failed.
---
Cargo.lock | 2 +
crates/uv-cli/src/compat.rs | 20 +-
crates/uv-cli/src/lib.rs | 112 +++++++++-
crates/uv-cli/src/options.rs | 35 ++++
crates/uv-client/src/base_client.rs | 193 ++++++++++++------
crates/uv-client/src/cached_client.rs | 8 +-
crates/uv-client/src/flat_index.rs | 2 +-
crates/uv-client/src/registry_client.rs | 27 ++-
crates/uv-client/tests/user_agent_version.rs | 2 +
crates/uv-configuration/Cargo.toml | 2 +
crates/uv-configuration/src/lib.rs | 2 +
crates/uv-configuration/src/trusted_host.rs | 137 +++++++++++++
.../src/distribution_database.rs | 2 +-
crates/uv-distribution/src/source/mod.rs | 6 +-
crates/uv-python/src/downloads.rs | 2 +-
crates/uv-settings/src/settings.rs | 40 ++++
crates/uv/src/commands/pip/compile.rs | 6 +-
crates/uv/src/commands/pip/install.rs | 8 +-
crates/uv/src/commands/pip/sync.rs | 8 +-
crates/uv/src/commands/pip/uninstall.rs | 6 +-
crates/uv/src/commands/project/lock.rs | 2 +
crates/uv/src/commands/project/mod.rs | 8 +
crates/uv/src/commands/project/sync.rs | 2 +
crates/uv/src/commands/venv.rs | 6 +-
crates/uv/src/lib.rs | 5 +
crates/uv/src/settings.rs | 35 +++-
crates/uv/tests/show_settings.rs | 23 +++
docs/configuration/authentication.md | 17 ++
docs/reference/cli.md | 126 +++++++++++-
docs/reference/settings.md | 65 ++++++
uv.schema.json | 24 +++
31 files changed, 808 insertions(+), 125 deletions(-)
create mode 100644 crates/uv-configuration/src/trusted_host.rs
diff --git a/Cargo.lock b/Cargo.lock
index 915ddc34a..31bd8ab4e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4774,7 +4774,9 @@ dependencies = [
"schemars",
"serde",
"serde_json",
+ "thiserror",
"tracing",
+ "url",
"uv-auth",
"uv-cache",
"uv-normalize",
diff --git a/crates/uv-cli/src/compat.rs b/crates/uv-cli/src/compat.rs
index 518e480d5..8f0147157 100644
--- a/crates/uv-cli/src/compat.rs
+++ b/crates/uv-cli/src/compat.rs
@@ -39,9 +39,6 @@ pub struct PipCompileCompatArgs {
#[clap(long, hide = true)]
client_cert: Option,
- #[clap(long, hide = true)]
- trusted_host: Option,
-
#[clap(long, hide = true)]
emit_trusted_host: bool,
@@ -118,15 +115,9 @@ impl CompatArgs for PipCompileCompatArgs {
));
}
- if self.trusted_host.is_some() {
- return Err(anyhow!(
- "pip-compile's `--trusted-host` is unsupported (uv always requires HTTPS)"
- ));
- }
-
if self.emit_trusted_host {
return Err(anyhow!(
- "pip-compile's `--emit-trusted-host` is unsupported (uv always requires HTTPS)"
+ "pip-compile's `--emit-trusted-host` is unsupported"
));
}
@@ -209,9 +200,6 @@ pub struct PipSyncCompatArgs {
#[clap(short, long, hide = true)]
ask: bool,
- #[clap(long, hide = true)]
- trusted_host: Option,
-
#[clap(long, hide = true)]
python_executable: Option,
@@ -265,12 +253,6 @@ impl CompatArgs for PipSyncCompatArgs {
));
}
- if self.trusted_host.is_some() {
- return Err(anyhow!(
- "pip-sync's `--trusted-host` is unsupported (uv always requires HTTPS)"
- ));
- }
-
if self.config.is_some() {
return Err(anyhow!(
"pip-sync's `--config` is unsupported (uv does not use a configuration file)"
diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs
index 593d030f9..b68666f2c 100644
--- a/crates/uv-cli/src/lib.rs
+++ b/crates/uv-cli/src/lib.rs
@@ -6,13 +6,13 @@ use std::str::FromStr;
use anyhow::{anyhow, Result};
use clap::builder::styling::Style;
use clap::{Args, Parser, Subcommand};
-
use distribution_types::{FlatIndexLocation, IndexUrl};
use pep508_rs::Requirement;
use pypi_types::VerbatimParsedUrl;
use uv_cache::CacheArgs;
use uv_configuration::{
ConfigSettingEntry, IndexStrategy, KeyringProviderType, PackageNameSpecifier, TargetTriple,
+ TrustedHost,
};
use uv_normalize::{ExtraName, PackageName};
use uv_python::{PythonDownloads, PythonPreference, PythonVersion};
@@ -678,6 +678,18 @@ fn parse_index_url(input: &str) -> Result, String> {
}
}
+/// Parse a string into an [`Url`], mapping the empty string to `None`.
+fn parse_insecure_host(input: &str) -> Result, String> {
+ if input.is_empty() {
+ Ok(Maybe::None)
+ } else {
+ match TrustedHost::from_str(input) {
+ Ok(host) => Ok(Maybe::Some(host)),
+ Err(err) => Err(err.to_string()),
+ }
+ }
+}
+
/// Parse a string into a [`PathBuf`]. The string can represent a file, either as a path or a
/// `file://` URL.
fn parse_file_path(input: &str) -> Result {
@@ -1559,6 +1571,25 @@ pub struct PipUninstallArgs {
#[arg(long, value_enum, env = "UV_KEYRING_PROVIDER")]
pub keyring_provider: Option,
+ /// Allow insecure connections to a host.
+ ///
+ /// Can be provided multiple times.
+ ///
+ /// Expects to receive either a hostname (e.g., `localhost`), a host-port pair (e.g.,
+ /// `localhost:8080`), or a URL (e.g., `https://localhost`).
+ ///
+ /// WARNING: Hosts included in this list will not be verified against the system's certificate
+ /// store. Only use `--allow-insecure-host` in a secure network with verified sources, as it
+ /// bypasses SSL verification and could expose you to MITM attacks.
+ #[arg(
+ long,
+ alias = "trusted-host",
+ env = "UV_INSECURE_HOST",
+ value_delimiter = ' ',
+ value_parser = parse_insecure_host,
+ )]
+ pub allow_insecure_host: Option>>,
+
/// Use the system Python to uninstall packages.
///
/// By default, uv uninstalls from the virtual environment in the current working directory or
@@ -1985,6 +2016,25 @@ pub struct VenvArgs {
#[arg(long, value_enum, env = "UV_KEYRING_PROVIDER")]
pub keyring_provider: Option,
+ /// Allow insecure connections to a host.
+ ///
+ /// Can be provided multiple times.
+ ///
+ /// Expects to receive either a hostname (e.g., `localhost`), a host-port pair (e.g.,
+ /// `localhost:8080`), or a URL (e.g., `https://localhost`).
+ ///
+ /// WARNING: Hosts included in this list will not be verified against the system's certificate
+ /// store. Only use `--allow-insecure-host` in a secure network with verified sources, as it
+ /// bypasses SSL verification and could expose you to MITM attacks.
+ #[arg(
+ long,
+ alias = "trusted-host",
+ env = "UV_INSECURE_HOST",
+ value_delimiter = ' ',
+ value_parser = parse_insecure_host,
+ )]
+ pub allow_insecure_host: Option>>,
+
/// Limit candidate packages to those that were uploaded prior to the given date.
///
/// Accepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and local dates in the same
@@ -3321,6 +3371,26 @@ pub struct InstallerArgs {
)]
pub keyring_provider: Option,
+ /// Allow insecure connections to a host.
+ ///
+ /// Can be provided multiple times.
+ ///
+ /// Expects to receive either a hostname (e.g., `localhost`), a host-port pair (e.g.,
+ /// `localhost:8080`), or a URL (e.g., `https://localhost`).
+ ///
+ /// WARNING: Hosts included in this list will not be verified against the system's certificate
+ /// store. Only use `--allow-insecure-host` in a secure network with verified sources, as it
+ /// bypasses SSL verification and could expose you to MITM attacks.
+ #[arg(
+ long,
+ alias = "trusted-host",
+ env = "UV_INSECURE_HOST",
+ value_delimiter = ' ',
+ value_parser = parse_insecure_host,
+ help_heading = "Index options"
+ )]
+ pub allow_insecure_host: Option>>,
+
/// Settings to pass to the PEP 517 build backend, specified as `KEY=VALUE` pairs.
#[arg(
long,
@@ -3463,6 +3533,26 @@ pub struct ResolverArgs {
)]
pub keyring_provider: Option,
+ /// Allow insecure connections to a host.
+ ///
+ /// Can be provided multiple times.
+ ///
+ /// Expects to receive either a hostname (e.g., `localhost`), a host-port pair (e.g.,
+ /// `localhost:8080`), or a URL (e.g., `https://localhost`).
+ ///
+ /// WARNING: Hosts included in this list will not be verified against the system's certificate
+ /// store. Only use `--allow-insecure-host` in a secure network with verified sources, as it
+ /// bypasses SSL verification and could expose you to MITM attacks.
+ #[arg(
+ long,
+ alias = "trusted-host",
+ env = "UV_INSECURE_HOST",
+ value_delimiter = ' ',
+ value_parser = parse_insecure_host,
+ help_heading = "Index options"
+ )]
+ pub allow_insecure_host: Option>>,
+
/// The strategy to use when selecting between the different compatible versions for a given
/// package requirement.
///
@@ -3635,6 +3725,26 @@ pub struct ResolverInstallerArgs {
)]
pub keyring_provider: Option,
+ /// Allow insecure connections to a host.
+ ///
+ /// Can be provided multiple times.
+ ///
+ /// Expects to receive either a hostname (e.g., `localhost`), a host-port pair (e.g.,
+ /// `localhost:8080`), or a URL (e.g., `https://localhost`).
+ ///
+ /// WARNING: Hosts included in this list will not be verified against the system's certificate
+ /// store. Only use `--allow-insecure-host` in a secure network with verified sources, as it
+ /// bypasses SSL verification and could expose you to MITM attacks.
+ #[arg(
+ long,
+ alias = "trusted-host",
+ env = "UV_INSECURE_HOST",
+ value_delimiter = ' ',
+ value_parser = parse_insecure_host,
+ help_heading = "Index options"
+ )]
+ pub allow_insecure_host: Option>>,
+
/// The strategy to use when selecting between the different compatible versions for a given
/// package requirement.
///
diff --git a/crates/uv-cli/src/options.rs b/crates/uv-cli/src/options.rs
index bbf7133e9..6fb230b23 100644
--- a/crates/uv-cli/src/options.rs
+++ b/crates/uv-cli/src/options.rs
@@ -38,6 +38,7 @@ impl From for PipOptions {
upgrade_package,
index_strategy,
keyring_provider,
+ allow_insecure_host,
resolution,
prerelease,
pre,
@@ -55,6 +56,12 @@ impl From for PipOptions {
upgrade_package: Some(upgrade_package),
index_strategy,
keyring_provider,
+ allow_insecure_host: allow_insecure_host.map(|allow_insecure_host| {
+ allow_insecure_host
+ .into_iter()
+ .filter_map(Maybe::into_option)
+ .collect()
+ }),
resolution,
prerelease: if pre {
Some(PrereleaseMode::Allow)
@@ -82,6 +89,7 @@ impl From for PipOptions {
reinstall_package,
index_strategy,
keyring_provider,
+ allow_insecure_host,
config_setting,
no_build_isolation,
build_isolation,
@@ -97,6 +105,12 @@ impl From for PipOptions {
reinstall_package: Some(reinstall_package),
index_strategy,
keyring_provider,
+ allow_insecure_host: allow_insecure_host.map(|allow_insecure_host| {
+ allow_insecure_host
+ .into_iter()
+ .filter_map(Maybe::into_option)
+ .collect()
+ }),
config_settings: config_setting
.map(|config_settings| config_settings.into_iter().collect::()),
no_build_isolation: flag(no_build_isolation, build_isolation),
@@ -121,6 +135,7 @@ impl From for PipOptions {
reinstall_package,
index_strategy,
keyring_provider,
+ allow_insecure_host,
resolution,
prerelease,
pre,
@@ -142,6 +157,12 @@ impl From for PipOptions {
reinstall_package: Some(reinstall_package),
index_strategy,
keyring_provider,
+ allow_insecure_host: allow_insecure_host.map(|allow_insecure_host| {
+ allow_insecure_host
+ .into_iter()
+ .filter_map(Maybe::into_option)
+ .collect()
+ }),
resolution,
prerelease: if pre {
Some(PrereleaseMode::Allow)
@@ -194,6 +215,7 @@ pub fn resolver_options(resolver_args: ResolverArgs, build_args: BuildArgs) -> R
upgrade_package,
index_strategy,
keyring_provider,
+ allow_insecure_host,
resolution,
prerelease,
pre,
@@ -233,6 +255,12 @@ pub fn resolver_options(resolver_args: ResolverArgs, build_args: BuildArgs) -> R
upgrade_package: Some(upgrade_package),
index_strategy,
keyring_provider,
+ allow_insecure_host: allow_insecure_host.map(|allow_insecure_host| {
+ allow_insecure_host
+ .into_iter()
+ .filter_map(Maybe::into_option)
+ .collect()
+ }),
resolution,
prerelease: if pre {
Some(PrereleaseMode::Allow)
@@ -268,6 +296,7 @@ pub fn resolver_installer_options(
reinstall_package,
index_strategy,
keyring_provider,
+ allow_insecure_host,
resolution,
prerelease,
pre,
@@ -319,6 +348,12 @@ pub fn resolver_installer_options(
},
index_strategy,
keyring_provider,
+ allow_insecure_host: allow_insecure_host.map(|allow_insecure_host| {
+ allow_insecure_host
+ .into_iter()
+ .filter_map(Maybe::into_option)
+ .collect()
+ }),
resolution,
prerelease: if pre {
Some(PrereleaseMode::Allow)
diff --git a/crates/uv-client/src/base_client.rs b/crates/uv-client/src/base_client.rs
index c75ff72c1..4edbea7ae 100644
--- a/crates/uv-client/src/base_client.rs
+++ b/crates/uv-client/src/base_client.rs
@@ -1,10 +1,11 @@
use std::error::Error;
use std::fmt::Debug;
-use std::ops::Deref;
use std::path::Path;
use std::{env, iter};
use itertools::Itertools;
+use pep508_rs::MarkerEnvironment;
+use platform_tags::Platform;
use reqwest::{Client, ClientBuilder, Response};
use reqwest_middleware::ClientWithMiddleware;
use reqwest_retry::policies::ExponentialBackoff;
@@ -12,11 +13,9 @@ use reqwest_retry::{
DefaultRetryableStrategy, RetryTransientMiddleware, Retryable, RetryableStrategy,
};
use tracing::debug;
-
-use pep508_rs::MarkerEnvironment;
-use platform_tags::Platform;
+use url::Url;
use uv_auth::AuthMiddleware;
-use uv_configuration::KeyringProviderType;
+use uv_configuration::{KeyringProviderType, TrustedHost};
use uv_fs::Simplified;
use uv_version::version;
use uv_warnings::warn_user_once;
@@ -30,6 +29,7 @@ use crate::Connectivity;
#[derive(Debug, Clone)]
pub struct BaseClientBuilder<'a> {
keyring: KeyringProviderType,
+ allow_insecure_host: Vec,
native_tls: bool,
retries: u32,
pub connectivity: Connectivity,
@@ -48,6 +48,7 @@ impl BaseClientBuilder<'_> {
pub fn new() -> Self {
Self {
keyring: KeyringProviderType::default(),
+ allow_insecure_host: vec![],
native_tls: false,
connectivity: Connectivity::Online,
retries: 3,
@@ -65,6 +66,12 @@ impl<'a> BaseClientBuilder<'a> {
self
}
+ #[must_use]
+ pub fn allow_insecure_host(mut self, allow_insecure_host: Vec) -> Self {
+ self.allow_insecure_host = allow_insecure_host;
+ self
+ }
+
#[must_use]
pub fn connectivity(mut self, connectivity: Connectivity) -> Self {
self.connectivity = connectivity;
@@ -117,6 +124,18 @@ impl<'a> BaseClientBuilder<'a> {
}
}
+ // Check for the presence of an `SSL_CERT_FILE`.
+ let ssl_cert_file_exists = env::var_os("SSL_CERT_FILE").is_some_and(|path| {
+ let path_exists = Path::new(&path).exists();
+ if !path_exists {
+ warn_user_once!(
+ "Ignoring invalid `SSL_CERT_FILE`. File does not exist: {}.",
+ path.simplified_display().cyan()
+ );
+ }
+ path_exists
+ });
+
// Timeout options, matching https://doc.rust-lang.org/nightly/cargo/reference/config.html#httptimeout
// `UV_REQUEST_TIMEOUT` is provided for backwards compatibility with v0.1.6
let default_timeout = 30;
@@ -134,54 +153,83 @@ impl<'a> BaseClientBuilder<'a> {
.unwrap_or(default_timeout);
debug!("Using request timeout of {timeout}s");
- // Initialize the base client.
- let client = self.client.clone().unwrap_or_else(|| {
- // Check for the presence of an `SSL_CERT_FILE`.
- let ssl_cert_file_exists = env::var_os("SSL_CERT_FILE").is_some_and(|path| {
- let path_exists = Path::new(&path).exists();
- if !path_exists {
- warn_user_once!(
- "Ignoring invalid `SSL_CERT_FILE`. File does not exist: {}.",
- path.simplified_display().cyan()
- );
+ // Create a secure client that validates certificates.
+ let client = self.create_client(
+ &user_agent_string,
+ timeout,
+ ssl_cert_file_exists,
+ Security::Secure,
+ );
+
+ // Create an insecure client that accepts invalid certificates.
+ let dangerous_client = self.create_client(
+ &user_agent_string,
+ timeout,
+ ssl_cert_file_exists,
+ Security::Insecure,
+ );
+
+ // Wrap in any relevant middleware and handle connectivity.
+ let client = self.apply_middleware(client);
+ let dangerous_client = self.apply_middleware(dangerous_client);
+
+ BaseClient {
+ connectivity: self.connectivity,
+ allow_insecure_host: self.allow_insecure_host.clone(),
+ client,
+ dangerous_client,
+ timeout,
+ }
+ }
+
+ fn create_client(
+ &self,
+ user_agent: &str,
+ timeout: u64,
+ ssl_cert_file_exists: bool,
+ security: Security,
+ ) -> Client {
+ // Configure the builder.
+ let client_builder = ClientBuilder::new()
+ .user_agent(user_agent)
+ .pool_max_idle_per_host(20)
+ .read_timeout(std::time::Duration::from_secs(timeout))
+ .tls_built_in_root_certs(false);
+
+ // If necessary, accept invalid certificates.
+ let client_builder = match security {
+ Security::Secure => client_builder,
+ Security::Insecure => client_builder.danger_accept_invalid_certs(true),
+ };
+
+ let client_builder = if self.native_tls || ssl_cert_file_exists {
+ client_builder.tls_built_in_native_certs(true)
+ } else {
+ client_builder.tls_built_in_webpki_certs(true)
+ };
+
+ // Configure mTLS.
+ let client_builder = if let Some(ssl_client_cert) = env::var_os("SSL_CLIENT_CERT") {
+ match read_identity(&ssl_client_cert) {
+ Ok(identity) => client_builder.identity(identity),
+ Err(err) => {
+ warn_user_once!("Ignoring invalid `SSL_CLIENT_CERT`: {err}");
+ client_builder
}
- path_exists
- });
+ }
+ } else {
+ client_builder
+ };
- // Configure the builder.
- let client_core = ClientBuilder::new()
- .user_agent(user_agent_string)
- .pool_max_idle_per_host(20)
- .read_timeout(std::time::Duration::from_secs(timeout))
- .tls_built_in_root_certs(false);
+ client_builder
+ .build()
+ .expect("Failed to build HTTP client.")
+ }
- // Configure TLS.
- let client_core = if self.native_tls || ssl_cert_file_exists {
- client_core.tls_built_in_native_certs(true)
- } else {
- client_core.tls_built_in_webpki_certs(true)
- };
-
- // Configure mTLS.
- let client_core = if let Some(ssl_client_cert) = env::var_os("SSL_CLIENT_CERT") {
- match read_identity(&ssl_client_cert) {
- Ok(identity) => client_core.identity(identity),
- Err(err) => {
- warn_user_once!("Ignoring invalid `SSL_CLIENT_CERT`: {err}");
- client_core
- }
- }
- } else {
- client_core
- };
-
- client_core.build().expect("Failed to build HTTP client")
- });
-
- // Wrap in any relevant middleware.
- let client = match self.connectivity {
+ fn apply_middleware(&self, client: Client) -> ClientWithMiddleware {
+ match self.connectivity {
Connectivity::Online => {
- let client = reqwest_middleware::ClientBuilder::new(client.clone());
+ let client = reqwest_middleware::ClientBuilder::new(client);
// Initialize the retry strategy.
let retry_policy =
@@ -198,15 +246,9 @@ impl<'a> BaseClientBuilder<'a> {
client.build()
}
- Connectivity::Offline => reqwest_middleware::ClientBuilder::new(client.clone())
+ Connectivity::Offline => reqwest_middleware::ClientBuilder::new(client)
.with(OfflineMiddleware)
.build(),
- };
-
- BaseClient {
- connectivity: self.connectivity,
- client,
- timeout,
}
}
}
@@ -214,20 +256,45 @@ impl<'a> BaseClientBuilder<'a> {
/// A base client for HTTP requests
#[derive(Debug, Clone)]
pub struct BaseClient {
- /// The underlying HTTP client.
+ /// The underlying HTTP client that enforces valid certificates.
client: ClientWithMiddleware,
+ /// The underlying HTTP client that accepts invalid certificates.
+ dangerous_client: ClientWithMiddleware,
/// The connectivity mode to use.
connectivity: Connectivity,
/// Configured client timeout, in seconds.
timeout: u64,
+ /// Hosts that are trusted to use the insecure client.
+ allow_insecure_host: Vec,
+}
+
+#[derive(Debug, Clone, Copy)]
+enum Security {
+ /// The client should use secure settings, i.e., valid certificates.
+ Secure,
+ /// The client should use insecure settings, i.e., skip certificate validation.
+ Insecure,
}
impl BaseClient {
- /// The underlying [`ClientWithMiddleware`].
+ /// The underlying [`ClientWithMiddleware`] for secure requests.
pub fn client(&self) -> ClientWithMiddleware {
self.client.clone()
}
+ /// Selects the appropriate client based on the host's trustworthiness.
+ pub fn for_host(&self, url: &Url) -> &ClientWithMiddleware {
+ if self
+ .allow_insecure_host
+ .iter()
+ .any(|allow_insecure_host| allow_insecure_host.matches(url))
+ {
+ &self.dangerous_client
+ } else {
+ &self.client
+ }
+ }
+
/// The configured client timeout, in seconds.
pub fn timeout(&self) -> u64 {
self.timeout
@@ -239,16 +306,6 @@ impl BaseClient {
}
}
-// To avoid excessively verbose call chains, as the [`BaseClient`] is often nested within other client types.
-impl Deref for BaseClient {
- type Target = ClientWithMiddleware;
-
- /// Deference to the underlying [`ClientWithMiddleware`].
- fn deref(&self) -> &Self::Target {
- &self.client
- }
-}
-
/// Extends [`DefaultRetryableStrategy`], to log transient request failures and additional retry cases.
struct UvRetryableStrategy;
diff --git a/crates/uv-client/src/cached_client.rs b/crates/uv-client/src/cached_client.rs
index 21b8ef724..f4a8385d2 100644
--- a/crates/uv-client/src/cached_client.rs
+++ b/crates/uv-client/src/cached_client.rs
@@ -165,9 +165,9 @@ impl CachedClient {
Self(client)
}
- /// The base client
- pub fn uncached(&self) -> BaseClient {
- self.0.clone()
+ /// The underlying [`BaseClient`] without caching.
+ pub fn uncached(&self) -> &BaseClient {
+ &self.0
}
/// Make a cached request with a custom response transformation
@@ -460,6 +460,7 @@ impl CachedClient {
debug!("Sending revalidation request for: {url}");
let response = self
.0
+ .for_host(req.url())
.execute(req)
.instrument(info_span!("revalidation_request", url = url.as_str()))
.await
@@ -499,6 +500,7 @@ impl CachedClient {
let cache_policy_builder = CachePolicyBuilder::new(&req);
let response = self
.0
+ .for_host(req.url())
.execute(req)
.await
.map_err(ErrorKind::from)?
diff --git a/crates/uv-client/src/flat_index.rs b/crates/uv-client/src/flat_index.rs
index 8fe4abe28..94deec2bd 100644
--- a/crates/uv-client/src/flat_index.rs
+++ b/crates/uv-client/src/flat_index.rs
@@ -154,7 +154,7 @@ impl<'a> FlatIndexClient<'a> {
let flat_index_request = self
.client
- .uncached_client()
+ .uncached_client(url)
.get(url.clone())
.header("Accept-Encoding", "gzip")
.header("Accept", "text/html")
diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs
index 46c96a96b..9e9acb8c9 100644
--- a/crates/uv-client/src/registry_client.rs
+++ b/crates/uv-client/src/registry_client.rs
@@ -7,6 +7,7 @@ use async_http_range_reader::AsyncHttpRangeReader;
use futures::{FutureExt, TryStreamExt};
use http::HeaderMap;
use reqwest::{Client, Response, StatusCode};
+use reqwest_middleware::ClientWithMiddleware;
use serde::{Deserialize, Serialize};
use tokio::io::AsyncReadExt;
use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt};
@@ -21,11 +22,11 @@ use pep508_rs::MarkerEnvironment;
use platform_tags::Platform;
use pypi_types::{Metadata23, SimpleJson};
use uv_cache::{Cache, CacheBucket, CacheEntry, WheelCache};
-use uv_configuration::IndexStrategy;
use uv_configuration::KeyringProviderType;
+use uv_configuration::{IndexStrategy, TrustedHost};
use uv_normalize::PackageName;
-use crate::base_client::{BaseClient, BaseClientBuilder};
+use crate::base_client::BaseClientBuilder;
use crate::cached_client::CacheControl;
use crate::html::SimpleHtml;
use crate::remote_metadata::wheel_metadata_from_remote_zip;
@@ -71,6 +72,14 @@ impl<'a> RegistryClientBuilder<'a> {
self
}
+ #[must_use]
+ pub fn allow_insecure_host(mut self, allow_insecure_host: Vec) -> Self {
+ self.base_client_builder = self
+ .base_client_builder
+ .allow_insecure_host(allow_insecure_host);
+ self
+ }
+
#[must_use]
pub fn connectivity(mut self, connectivity: Connectivity) -> Self {
self.base_client_builder = self.base_client_builder.connectivity(connectivity);
@@ -171,8 +180,8 @@ impl RegistryClient {
}
/// Return the [`BaseClient`] used by this client.
- pub fn uncached_client(&self) -> BaseClient {
- self.client.uncached()
+ pub fn uncached_client(&self, url: &Url) -> &ClientWithMiddleware {
+ self.client.uncached().for_host(url)
}
/// Return the [`Connectivity`] mode used by this client.
@@ -298,7 +307,7 @@ impl RegistryClient {
cache_control: CacheControl,
) -> Result, Error> {
let simple_request = self
- .uncached_client()
+ .uncached_client(url)
.get(url.clone())
.header("Accept-Encoding", "gzip")
.header("Accept", MediaType::accepts())
@@ -512,7 +521,7 @@ impl RegistryClient {
})
};
let req = self
- .uncached_client()
+ .uncached_client(&url)
.get(url.clone())
.build()
.map_err(ErrorKind::from)?;
@@ -551,7 +560,7 @@ impl RegistryClient {
};
let req = self
- .uncached_client()
+ .uncached_client(url)
.head(url.clone())
.header(
"accept-encoding",
@@ -571,7 +580,7 @@ impl RegistryClient {
let read_metadata_range_request = |response: Response| {
async {
let mut reader = AsyncHttpRangeReader::from_head_response(
- self.uncached_client().client(),
+ self.uncached_client(url).clone(),
response,
url.clone(),
headers,
@@ -619,7 +628,7 @@ impl RegistryClient {
// Create a request to stream the file.
let req = self
- .uncached_client()
+ .uncached_client(url)
.get(url.clone())
.header(
// `reqwest` defaults to accepting compressed responses.
diff --git a/crates/uv-client/tests/user_agent_version.rs b/crates/uv-client/tests/user_agent_version.rs
index 7762d00cf..26632a78b 100644
--- a/crates/uv-client/tests/user_agent_version.rs
+++ b/crates/uv-client/tests/user_agent_version.rs
@@ -56,6 +56,7 @@ async fn test_user_agent_has_version() -> Result<()> {
let res = client
.cached_client()
.uncached()
+ .client()
.get(format!("http://{addr}"))
.send()
.await?;
@@ -151,6 +152,7 @@ async fn test_user_agent_has_linehaul() -> Result<()> {
let res = client
.cached_client()
.uncached()
+ .client()
.get(format!("http://{addr}"))
.send()
.await?;
diff --git a/crates/uv-configuration/Cargo.toml b/crates/uv-configuration/Cargo.toml
index 69208f363..f973428cb 100644
--- a/crates/uv-configuration/Cargo.toml
+++ b/crates/uv-configuration/Cargo.toml
@@ -28,7 +28,9 @@ rustc-hash = { workspace = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true }
serde_json = { workspace = true }
+thiserror = { workspace = true }
tracing = { workspace = true }
+url = { workspace = true }
[dev-dependencies]
anyhow = { workspace = true }
diff --git a/crates/uv-configuration/src/lib.rs b/crates/uv-configuration/src/lib.rs
index 38a1d37ac..bdfdf67f5 100644
--- a/crates/uv-configuration/src/lib.rs
+++ b/crates/uv-configuration/src/lib.rs
@@ -12,6 +12,7 @@ pub use package_options::*;
pub use preview::*;
pub use sources::*;
pub use target_triple::*;
+pub use trusted_host::*;
mod authentication;
mod build_options;
@@ -27,3 +28,4 @@ mod package_options;
mod preview;
mod sources;
mod target_triple;
+mod trusted_host;
diff --git a/crates/uv-configuration/src/trusted_host.rs b/crates/uv-configuration/src/trusted_host.rs
new file mode 100644
index 000000000..910fe6991
--- /dev/null
+++ b/crates/uv-configuration/src/trusted_host.rs
@@ -0,0 +1,137 @@
+use serde::{Deserialize, Serialize};
+use url::Url;
+
+/// A trusted host, which could be a host or a host-port pair.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct TrustedHost {
+ scheme: Option,
+ host: String,
+ port: Option,
+}
+
+impl TrustedHost {
+ /// Returns `true` if the [`Url`] matches this trusted host.
+ pub fn matches(&self, url: &Url) -> bool {
+ if self
+ .scheme
+ .as_ref()
+ .is_some_and(|scheme| scheme != url.scheme())
+ {
+ return false;
+ }
+
+ if self.port.is_some_and(|port| url.port() != Some(port)) {
+ return false;
+ }
+
+ if Some(self.host.as_ref()) != url.host_str() {
+ return false;
+ }
+
+ true
+ }
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum TrustedHostError {
+ #[error("missing host for `--trusted-host`: `{0}`")]
+ MissingHost(String),
+ #[error("invalid port for `--trusted-host`: `{0}`")]
+ InvalidPort(String),
+}
+
+impl std::str::FromStr for TrustedHost {
+ type Err = TrustedHostError;
+
+ fn from_str(s: &str) -> Result {
+ // Detect scheme.
+ let (scheme, s) = if let Some(s) = s.strip_prefix("https://") {
+ (Some("https".to_string()), s)
+ } else if let Some(s) = s.strip_prefix("http://") {
+ (Some("http".to_string()), s)
+ } else {
+ (None, s)
+ };
+
+ let mut parts = s.splitn(2, ':');
+
+ // Detect host.
+ let host = parts
+ .next()
+ .and_then(|host| host.split('/').next())
+ .map(ToString::to_string)
+ .ok_or_else(|| TrustedHostError::MissingHost(s.to_string()))?;
+
+ // Detect port.
+ let port = parts
+ .next()
+ .map(str::parse)
+ .transpose()
+ .map_err(|_| TrustedHostError::InvalidPort(s.to_string()))?;
+
+ Ok(Self { scheme, host, port })
+ }
+}
+
+#[cfg(feature = "schemars")]
+impl schemars::JsonSchema for TrustedHost {
+ fn schema_name() -> String {
+ "TrustedHost".to_string()
+ }
+
+ fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
+ schemars::schema::SchemaObject {
+ instance_type: Some(schemars::schema::InstanceType::String.into()),
+ metadata: Some(Box::new(schemars::schema::Metadata {
+ description: Some("A host or host-port pair.".to_string()),
+ ..schemars::schema::Metadata::default()
+ })),
+ ..schemars::schema::SchemaObject::default()
+ }
+ .into()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ #[test]
+ fn parse() {
+ assert_eq!(
+ "example.com".parse::().unwrap(),
+ super::TrustedHost {
+ scheme: None,
+ host: "example.com".to_string(),
+ port: None
+ }
+ );
+
+ assert_eq!(
+ "example.com:8080".parse::().unwrap(),
+ super::TrustedHost {
+ scheme: None,
+ host: "example.com".to_string(),
+ port: Some(8080)
+ }
+ );
+
+ assert_eq!(
+ "https://example.com".parse::().unwrap(),
+ super::TrustedHost {
+ scheme: Some("https".to_string()),
+ host: "example.com".to_string(),
+ port: None
+ }
+ );
+
+ assert_eq!(
+ "https://example.com/hello/world"
+ .parse::()
+ .unwrap(),
+ super::TrustedHost {
+ scheme: Some("https".to_string()),
+ host: "example.com".to_string(),
+ port: None
+ }
+ );
+ }
+}
diff --git a/crates/uv-distribution/src/distribution_database.rs b/crates/uv-distribution/src/distribution_database.rs
index 0b4660886..86b537992 100644
--- a/crates/uv-distribution/src/distribution_database.rs
+++ b/crates/uv-distribution/src/distribution_database.rs
@@ -834,7 +834,7 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> {
fn request(&self, url: Url) -> Result {
self.client
.unmanaged
- .uncached_client()
+ .uncached_client(&url)
.get(url)
.header(
// `reqwest` defaults to accepting compressed responses.
diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs
index 6d4a36ea6..af40aec90 100644
--- a/crates/uv-distribution/src/source/mod.rs
+++ b/crates/uv-distribution/src/source/mod.rs
@@ -1118,7 +1118,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
.git()
.fetch(
resource.git,
- client.unmanaged.uncached_client().client(),
+ client.unmanaged.uncached_client(resource.url).clone(),
self.build_context.cache().bucket(CacheBucket::Git),
self.reporter.clone().map(Facade::from),
)
@@ -1188,7 +1188,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
.git()
.fetch(
resource.git,
- client.unmanaged.uncached_client().client(),
+ client.unmanaged.uncached_client(resource.url).clone(),
self.build_context.cache().bucket(CacheBucket::Git),
self.reporter.clone().map(Facade::from),
)
@@ -1589,7 +1589,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
/// Returns a GET [`reqwest::Request`] for the given URL.
fn request(url: Url, client: &RegistryClient) -> Result {
client
- .uncached_client()
+ .uncached_client(&url)
.get(url)
.header(
// `reqwest` defaults to accepting compressed responses.
diff --git a/crates/uv-python/src/downloads.rs b/crates/uv-python/src/downloads.rs
index c5a5c20c2..af9ef7fe8 100644
--- a/crates/uv-python/src/downloads.rs
+++ b/crates/uv-python/src/downloads.rs
@@ -432,7 +432,7 @@ impl ManagedPythonDownload {
let filename = url.path_segments().unwrap().last().unwrap();
let ext = SourceDistExtension::from_path(filename)
.map_err(|err| Error::MissingExtension(url.to_string(), err))?;
- let response = client.get(url.clone()).send().await?;
+ let response = client.client().get(url.clone()).send().await?;
// Ensure the request was successful.
response.error_for_status_ref()?;
diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs
index 73ef373c9..273a1d8c0 100644
--- a/crates/uv-settings/src/settings.rs
+++ b/crates/uv-settings/src/settings.rs
@@ -8,6 +8,7 @@ use pep508_rs::Requirement;
use pypi_types::{SupportedEnvironments, VerbatimParsedUrl};
use uv_configuration::{
ConfigSettings, IndexStrategy, KeyringProviderType, PackageNameSpecifier, TargetTriple,
+ TrustedHost,
};
use uv_macros::{CombineOptions, OptionsMetadata};
use uv_normalize::{ExtraName, PackageName};
@@ -213,6 +214,7 @@ pub struct InstallerOptions {
pub find_links: Option>,
pub index_strategy: Option,
pub keyring_provider: Option,
+ pub allow_insecure_host: Option>,
pub config_settings: Option,
pub exclude_newer: Option,
pub link_mode: Option,
@@ -239,6 +241,7 @@ pub struct ResolverOptions {
pub find_links: Option>,
pub index_strategy: Option,
pub keyring_provider: Option,
+ pub allow_insecure_host: Option>,
pub resolution: Option,
pub prerelease: Option,
pub config_settings: Option,
@@ -350,6 +353,22 @@ pub struct ResolverInstallerOptions {
"#
)]
pub keyring_provider: Option,
+ /// Allow insecure connections to host.
+ ///
+ /// Expects to receive either a hostname (e.g., `localhost`), a host-port pair (e.g.,
+ /// `localhost:8080`), or a URL (e.g., `https://localhost`).
+ ///
+ /// WARNING: Hosts included in this list will not be verified against the system's certificate
+ /// store. Only use `--allow-insecure-host` in a secure network with verified sources, as it
+ /// bypasses SSL verification and could expose you to MITM attacks.
+ #[option(
+ default = "[]",
+ value_type = "list[str]",
+ example = r#"
+ allow-insecure-host = ["localhost:8080"]
+ "#
+ )]
+ pub allow_insecure_host: Option>,
/// The strategy to use when selecting between the different compatible versions for a given
/// package requirement.
///
@@ -721,6 +740,22 @@ pub struct PipOptions {
"#
)]
pub keyring_provider: Option,
+ /// Allow insecure connections to host.
+ ///
+ /// Expects to receive either a hostname (e.g., `localhost`), a host-port pair (e.g.,
+ /// `localhost:8080`), or a URL (e.g., `https://localhost`).
+ ///
+ /// WARNING: Hosts included in this list will not be verified against the system's certificate
+ /// store. Only use `--allow-insecure-host` in a secure network with verified sources, as it
+ /// bypasses SSL verification and could expose you to MITM attacks.
+ #[option(
+ default = "[]",
+ value_type = "list[str]",
+ example = r#"
+ allow-insecure-host = ["localhost:8080"]
+ "#
+ )]
+ pub allow_insecure_host: Option>,
/// Don't build source distributions.
///
/// When enabled, resolving will not run arbitrary Python code. The cached wheels of
@@ -1208,6 +1243,7 @@ impl From for ResolverOptions {
find_links: value.find_links,
index_strategy: value.index_strategy,
keyring_provider: value.keyring_provider,
+ allow_insecure_host: value.allow_insecure_host,
resolution: value.resolution,
prerelease: value.prerelease,
config_settings: value.config_settings,
@@ -1235,6 +1271,7 @@ impl From for InstallerOptions {
find_links: value.find_links,
index_strategy: value.index_strategy,
keyring_provider: value.keyring_provider,
+ allow_insecure_host: value.allow_insecure_host,
config_settings: value.config_settings,
exclude_newer: value.exclude_newer,
link_mode: value.link_mode,
@@ -1267,6 +1304,7 @@ pub struct ToolOptions {
pub find_links: Option>,
pub index_strategy: Option,
pub keyring_provider: Option,
+ pub allow_insecure_host: Option>,
pub resolution: Option,
pub prerelease: Option,
pub config_settings: Option,
@@ -1291,6 +1329,7 @@ impl From for ToolOptions {
find_links: value.find_links,
index_strategy: value.index_strategy,
keyring_provider: value.keyring_provider,
+ allow_insecure_host: value.allow_insecure_host,
resolution: value.resolution,
prerelease: value.prerelease,
config_settings: value.config_settings,
@@ -1317,6 +1356,7 @@ impl From for ResolverInstallerOptions {
find_links: value.find_links,
index_strategy: value.index_strategy,
keyring_provider: value.keyring_provider,
+ allow_insecure_host: value.allow_insecure_host,
resolution: value.resolution,
prerelease: value.prerelease,
config_settings: value.config_settings,
diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs
index aedff28a2..99cfb2ff7 100644
--- a/crates/uv/src/commands/pip/compile.rs
+++ b/crates/uv/src/commands/pip/compile.rs
@@ -17,7 +17,7 @@ use uv_cache::Cache;
use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder};
use uv_configuration::{
BuildOptions, Concurrency, ConfigSettings, ExtrasSpecification, IndexStrategy, NoBinary,
- NoBuild, Reinstall, SourceStrategy, Upgrade,
+ NoBuild, Reinstall, SourceStrategy, TrustedHost, Upgrade,
};
use uv_configuration::{KeyringProviderType, TargetTriple};
use uv_dispatch::BuildDispatch;
@@ -75,6 +75,7 @@ pub(crate) async fn pip_compile(
index_locations: IndexLocations,
index_strategy: IndexStrategy,
keyring_provider: KeyringProviderType,
+ allow_insecure_host: Vec,
config_settings: ConfigSettings,
connectivity: Connectivity,
no_build_isolation: bool,
@@ -107,7 +108,8 @@ pub(crate) async fn pip_compile(
let client_builder = BaseClientBuilder::new()
.connectivity(connectivity)
.native_tls(native_tls)
- .keyring(keyring_provider);
+ .keyring(keyring_provider)
+ .allow_insecure_host(allow_insecure_host);
// Read all requirements from the provided sources.
let RequirementsSpecification {
diff --git a/crates/uv/src/commands/pip/install.rs b/crates/uv/src/commands/pip/install.rs
index a52fb7ce3..f82a10317 100644
--- a/crates/uv/src/commands/pip/install.rs
+++ b/crates/uv/src/commands/pip/install.rs
@@ -3,18 +3,18 @@ use std::fmt::Write;
use anstream::eprint;
use itertools::Itertools;
use owo_colors::OwoColorize;
-use pep508_rs::PackageName;
use tracing::{debug, enabled, Level};
use distribution_types::{IndexLocations, Resolution, UnresolvedRequirementSpecification};
use install_wheel_rs::linker::LinkMode;
+use pep508_rs::PackageName;
use pypi_types::Requirement;
use uv_auth::store_credentials_from_url;
use uv_cache::Cache;
use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder};
use uv_configuration::{
BuildOptions, Concurrency, ConfigSettings, ExtrasSpecification, HashCheckingMode,
- IndexStrategy, Reinstall, SourceStrategy, Upgrade,
+ IndexStrategy, Reinstall, SourceStrategy, TrustedHost, Upgrade,
};
use uv_configuration::{KeyringProviderType, TargetTriple};
use uv_dispatch::BuildDispatch;
@@ -53,6 +53,7 @@ pub(crate) async fn pip_install(
index_locations: IndexLocations,
index_strategy: IndexStrategy,
keyring_provider: KeyringProviderType,
+ allow_insecure_host: Vec,
reinstall: Reinstall,
link_mode: LinkMode,
compile: bool,
@@ -83,7 +84,8 @@ pub(crate) async fn pip_install(
let client_builder = BaseClientBuilder::new()
.connectivity(connectivity)
.native_tls(native_tls)
- .keyring(keyring_provider);
+ .keyring(keyring_provider)
+ .allow_insecure_host(allow_insecure_host);
// Read all requirements from the provided sources.
let RequirementsSpecification {
diff --git a/crates/uv/src/commands/pip/sync.rs b/crates/uv/src/commands/pip/sync.rs
index f62461062..e6f43d6b0 100644
--- a/crates/uv/src/commands/pip/sync.rs
+++ b/crates/uv/src/commands/pip/sync.rs
@@ -3,17 +3,17 @@ use std::fmt::Write;
use anstream::eprint;
use anyhow::Result;
use owo_colors::OwoColorize;
-use pep508_rs::PackageName;
use tracing::debug;
use distribution_types::{IndexLocations, Resolution};
use install_wheel_rs::linker::LinkMode;
+use pep508_rs::PackageName;
use uv_auth::store_credentials_from_url;
use uv_cache::Cache;
use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder};
use uv_configuration::{
BuildOptions, Concurrency, ConfigSettings, ExtrasSpecification, HashCheckingMode,
- IndexStrategy, Reinstall, SourceStrategy, Upgrade,
+ IndexStrategy, Reinstall, SourceStrategy, TrustedHost, Upgrade,
};
use uv_configuration::{KeyringProviderType, TargetTriple};
use uv_dispatch::BuildDispatch;
@@ -48,6 +48,7 @@ pub(crate) async fn pip_sync(
index_locations: IndexLocations,
index_strategy: IndexStrategy,
keyring_provider: KeyringProviderType,
+ allow_insecure_host: Vec,
allow_empty_requirements: bool,
connectivity: Connectivity,
config_settings: &ConfigSettings,
@@ -73,7 +74,8 @@ pub(crate) async fn pip_sync(
let client_builder = BaseClientBuilder::new()
.connectivity(connectivity)
.native_tls(native_tls)
- .keyring(keyring_provider);
+ .keyring(keyring_provider)
+ .allow_insecure_host(allow_insecure_host);
// Initialize a few defaults.
let overrides = &[];
diff --git a/crates/uv/src/commands/pip/uninstall.rs b/crates/uv/src/commands/pip/uninstall.rs
index 4bfa05a0a..d5ef50775 100644
--- a/crates/uv/src/commands/pip/uninstall.rs
+++ b/crates/uv/src/commands/pip/uninstall.rs
@@ -11,7 +11,7 @@ use pypi_types::Requirement;
use pypi_types::VerbatimParsedUrl;
use uv_cache::Cache;
use uv_client::{BaseClientBuilder, Connectivity};
-use uv_configuration::KeyringProviderType;
+use uv_configuration::{KeyringProviderType, TrustedHost};
use uv_fs::Simplified;
use uv_python::EnvironmentPreference;
use uv_python::PythonRequest;
@@ -33,6 +33,7 @@ pub(crate) async fn pip_uninstall(
connectivity: Connectivity,
native_tls: bool,
keyring_provider: KeyringProviderType,
+ allow_insecure_host: Vec,
printer: Printer,
) -> Result {
let start = std::time::Instant::now();
@@ -40,7 +41,8 @@ pub(crate) async fn pip_uninstall(
let client_builder = BaseClientBuilder::new()
.connectivity(connectivity)
.native_tls(native_tls)
- .keyring(keyring_provider);
+ .keyring(keyring_provider)
+ .allow_insecure_host(allow_insecure_host);
// Read all requirements from the provided sources.
let spec = RequirementsSpecification::from_simple_sources(sources, &client_builder).await?;
diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs
index ea20a927c..37ca4f70e 100644
--- a/crates/uv/src/commands/project/lock.rs
+++ b/crates/uv/src/commands/project/lock.rs
@@ -234,6 +234,7 @@ async fn do_lock(
index_locations,
index_strategy,
keyring_provider,
+ allow_insecure_host,
resolution,
prerelease,
config_setting,
@@ -342,6 +343,7 @@ async fn do_lock(
.index_urls(index_locations.index_urls())
.index_strategy(index_strategy)
.keyring(keyring_provider)
+ .allow_insecure_host(allow_insecure_host.to_vec())
.markers(interpreter.markers())
.platform(interpreter.platform())
.build();
diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs
index ed56bd9ec..60c8ea9ce 100644
--- a/crates/uv/src/commands/project/mod.rs
+++ b/crates/uv/src/commands/project/mod.rs
@@ -410,6 +410,7 @@ pub(crate) async fn resolve_names(
index_locations,
index_strategy,
keyring_provider,
+ allow_insecure_host,
resolution: _,
prerelease: _,
config_setting,
@@ -436,6 +437,7 @@ pub(crate) async fn resolve_names(
.index_urls(index_locations.index_urls())
.index_strategy(*index_strategy)
.keyring(*keyring_provider)
+ .allow_insecure_host(allow_insecure_host.clone())
.markers(interpreter.markers())
.platform(interpreter.platform())
.build();
@@ -510,6 +512,7 @@ pub(crate) async fn resolve_environment<'a>(
index_locations,
index_strategy,
keyring_provider,
+ allow_insecure_host,
resolution,
prerelease,
config_setting,
@@ -549,6 +552,7 @@ pub(crate) async fn resolve_environment<'a>(
.index_urls(index_locations.index_urls())
.index_strategy(index_strategy)
.keyring(keyring_provider)
+ .allow_insecure_host(allow_insecure_host.to_vec())
.markers(interpreter.markers())
.platform(interpreter.platform())
.build();
@@ -660,6 +664,7 @@ pub(crate) async fn sync_environment(
index_locations,
index_strategy,
keyring_provider,
+ allow_insecure_host,
config_setting,
no_build_isolation,
no_build_isolation_package,
@@ -690,6 +695,7 @@ pub(crate) async fn sync_environment(
.index_urls(index_locations.index_urls())
.index_strategy(index_strategy)
.keyring(keyring_provider)
+ .allow_insecure_host(allow_insecure_host.to_vec())
.markers(interpreter.markers())
.platform(interpreter.platform())
.build();
@@ -804,6 +810,7 @@ pub(crate) async fn update_environment(
index_locations,
index_strategy,
keyring_provider,
+ allow_insecure_host,
resolution,
prerelease,
config_setting,
@@ -871,6 +878,7 @@ pub(crate) async fn update_environment(
.index_urls(index_locations.index_urls())
.index_strategy(*index_strategy)
.keyring(*keyring_provider)
+ .allow_insecure_host(allow_insecure_host.clone())
.markers(interpreter.markers())
.platform(interpreter.platform())
.build();
diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs
index 79431eec5..ef97e30fe 100644
--- a/crates/uv/src/commands/project/sync.rs
+++ b/crates/uv/src/commands/project/sync.rs
@@ -143,6 +143,7 @@ pub(super) async fn do_sync(
index_locations,
index_strategy,
keyring_provider,
+ allow_insecure_host,
config_setting,
no_build_isolation,
no_build_isolation_package,
@@ -209,6 +210,7 @@ pub(super) async fn do_sync(
.index_urls(index_locations.index_urls())
.index_strategy(index_strategy)
.keyring(keyring_provider)
+ .allow_insecure_host(allow_insecure_host.to_vec())
.markers(venv.interpreter().markers())
.platform(venv.interpreter().platform())
.build();
diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs
index 25772e3e1..eb4d57341 100644
--- a/crates/uv/src/commands/venv.rs
+++ b/crates/uv/src/commands/venv.rs
@@ -17,7 +17,7 @@ use uv_cache::Cache;
use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder};
use uv_configuration::{
BuildOptions, Concurrency, ConfigSettings, IndexStrategy, KeyringProviderType, NoBinary,
- NoBuild, SourceStrategy,
+ NoBuild, SourceStrategy, TrustedHost,
};
use uv_dispatch::BuildDispatch;
use uv_fs::{Simplified, CWD};
@@ -49,6 +49,7 @@ pub(crate) async fn venv(
index_locations: &IndexLocations,
index_strategy: IndexStrategy,
keyring_provider: KeyringProviderType,
+ allow_insecure_host: Vec,
prompt: uv_virtualenv::Prompt,
system_site_packages: bool,
connectivity: Connectivity,
@@ -69,6 +70,7 @@ pub(crate) async fn venv(
index_locations,
index_strategy,
keyring_provider,
+ allow_insecure_host,
prompt,
system_site_packages,
connectivity,
@@ -122,6 +124,7 @@ async fn venv_impl(
index_locations: &IndexLocations,
index_strategy: IndexStrategy,
keyring_provider: KeyringProviderType,
+ allow_insecure_host: Vec,
prompt: uv_virtualenv::Prompt,
system_site_packages: bool,
connectivity: Connectivity,
@@ -251,6 +254,7 @@ async fn venv_impl(
.index_urls(index_locations.index_urls())
.index_strategy(index_strategy)
.keyring(keyring_provider)
+ .allow_insecure_host(allow_insecure_host)
.markers(interpreter.markers())
.platform(interpreter.platform())
.build();
diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs
index b77ebf031..991fdd9fe 100644
--- a/crates/uv/src/lib.rs
+++ b/crates/uv/src/lib.rs
@@ -325,6 +325,7 @@ async fn run(cli: Cli) -> Result {
args.settings.index_locations,
args.settings.index_strategy,
args.settings.keyring_provider,
+ args.settings.allow_insecure_host,
args.settings.config_setting,
globals.connectivity,
args.settings.no_build_isolation,
@@ -391,6 +392,7 @@ async fn run(cli: Cli) -> Result {
args.settings.index_locations,
args.settings.index_strategy,
args.settings.keyring_provider,
+ args.settings.allow_insecure_host,
args.settings.allow_empty_requirements,
globals.connectivity,
&args.settings.config_setting,
@@ -474,6 +476,7 @@ async fn run(cli: Cli) -> Result {
args.settings.index_locations,
args.settings.index_strategy,
args.settings.keyring_provider,
+ args.settings.allow_insecure_host,
args.settings.reinstall,
args.settings.link_mode,
args.settings.compile_bytecode,
@@ -532,6 +535,7 @@ async fn run(cli: Cli) -> Result {
globals.connectivity,
globals.native_tls,
args.settings.keyring_provider,
+ args.settings.allow_insecure_host,
printer,
)
.await
@@ -692,6 +696,7 @@ async fn run(cli: Cli) -> Result {
&args.settings.index_locations,
args.settings.index_strategy,
args.settings.keyring_provider,
+ args.settings.allow_insecure_host,
uv_virtualenv::Prompt::from_args(prompt),
args.system_site_packages,
globals.connectivity,
diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs
index 4f7225f89..dc4c7b904 100644
--- a/crates/uv/src/settings.rs
+++ b/crates/uv/src/settings.rs
@@ -24,7 +24,7 @@ use uv_client::Connectivity;
use uv_configuration::{
BuildOptions, Concurrency, ConfigSettings, ExtrasSpecification, HashCheckingMode,
IndexStrategy, InstallOptions, KeyringProviderType, NoBinary, NoBuild, PreviewMode, Reinstall,
- SourceStrategy, TargetTriple, Upgrade,
+ SourceStrategy, TargetTriple, TrustedHost, Upgrade,
};
use uv_normalize::PackageName;
use uv_python::{Prefix, PythonDownloads, PythonPreference, PythonVersion, Target};
@@ -1308,6 +1308,7 @@ impl PipUninstallSettings {
requirement,
python,
keyring_provider,
+ allow_insecure_host,
system,
no_system,
break_system_packages,
@@ -1328,6 +1329,12 @@ impl PipUninstallSettings {
target,
prefix,
keyring_provider,
+ allow_insecure_host: allow_insecure_host.map(|allow_insecure_host| {
+ allow_insecure_host
+ .into_iter()
+ .filter_map(Maybe::into_option)
+ .collect()
+ }),
..PipOptions::default()
},
filesystem,
@@ -1558,6 +1565,7 @@ impl VenvSettings {
index_args,
index_strategy,
keyring_provider,
+ allow_insecure_host,
exclude_newer,
link_mode,
compat_args: _,
@@ -1576,6 +1584,12 @@ impl VenvSettings {
system: flag(system, no_system),
index_strategy,
keyring_provider,
+ allow_insecure_host: allow_insecure_host.map(|allow_insecure_host| {
+ allow_insecure_host
+ .into_iter()
+ .filter_map(Maybe::into_option)
+ .collect()
+ }),
exclude_newer,
link_mode,
..PipOptions::from(index_args)
@@ -1595,6 +1609,7 @@ pub(crate) struct InstallerSettingsRef<'a> {
pub(crate) index_locations: &'a IndexLocations,
pub(crate) index_strategy: IndexStrategy,
pub(crate) keyring_provider: KeyringProviderType,
+ pub(crate) allow_insecure_host: &'a [TrustedHost],
pub(crate) config_setting: &'a ConfigSettings,
pub(crate) no_build_isolation: bool,
pub(crate) no_build_isolation_package: &'a [PackageName],
@@ -1616,6 +1631,7 @@ pub(crate) struct ResolverSettings {
pub(crate) index_locations: IndexLocations,
pub(crate) index_strategy: IndexStrategy,
pub(crate) keyring_provider: KeyringProviderType,
+ pub(crate) allow_insecure_host: Vec,
pub(crate) resolution: ResolutionMode,
pub(crate) prerelease: PrereleaseMode,
pub(crate) config_setting: ConfigSettings,
@@ -1633,6 +1649,7 @@ pub(crate) struct ResolverSettingsRef<'a> {
pub(crate) index_locations: &'a IndexLocations,
pub(crate) index_strategy: IndexStrategy,
pub(crate) keyring_provider: KeyringProviderType,
+ pub(crate) allow_insecure_host: &'a [TrustedHost],
pub(crate) resolution: ResolutionMode,
pub(crate) prerelease: PrereleaseMode,
pub(crate) config_setting: &'a ConfigSettings,
@@ -1663,6 +1680,7 @@ impl ResolverSettings {
index_locations: &self.index_locations,
index_strategy: self.index_strategy,
keyring_provider: self.keyring_provider,
+ allow_insecure_host: &self.allow_insecure_host,
resolution: self.resolution,
prerelease: self.prerelease,
config_setting: &self.config_setting,
@@ -1690,6 +1708,7 @@ impl From for ResolverSettings {
prerelease: value.prerelease.unwrap_or_default(),
index_strategy: value.index_strategy.unwrap_or_default(),
keyring_provider: value.keyring_provider.unwrap_or_default(),
+ allow_insecure_host: value.allow_insecure_host.unwrap_or_default(),
config_setting: value.config_settings.unwrap_or_default(),
no_build_isolation: value.no_build_isolation.unwrap_or_default(),
no_build_isolation_package: value.no_build_isolation_package.unwrap_or_default(),
@@ -1718,6 +1737,7 @@ pub(crate) struct ResolverInstallerSettingsRef<'a> {
pub(crate) index_locations: &'a IndexLocations,
pub(crate) index_strategy: IndexStrategy,
pub(crate) keyring_provider: KeyringProviderType,
+ pub(crate) allow_insecure_host: &'a [TrustedHost],
pub(crate) resolution: ResolutionMode,
pub(crate) prerelease: PrereleaseMode,
pub(crate) config_setting: &'a ConfigSettings,
@@ -1744,6 +1764,7 @@ pub(crate) struct ResolverInstallerSettings {
pub(crate) index_locations: IndexLocations,
pub(crate) index_strategy: IndexStrategy,
pub(crate) keyring_provider: KeyringProviderType,
+ pub(crate) allow_insecure_host: Vec,
pub(crate) resolution: ResolutionMode,
pub(crate) prerelease: PrereleaseMode,
pub(crate) config_setting: ConfigSettings,
@@ -1779,6 +1800,7 @@ impl ResolverInstallerSettings {
index_locations: &self.index_locations,
index_strategy: self.index_strategy,
keyring_provider: self.keyring_provider,
+ allow_insecure_host: &self.allow_insecure_host,
resolution: self.resolution,
prerelease: self.prerelease,
config_setting: &self.config_setting,
@@ -1808,6 +1830,7 @@ impl From for ResolverInstallerSettings {
prerelease: value.prerelease.unwrap_or_default(),
index_strategy: value.index_strategy.unwrap_or_default(),
keyring_provider: value.keyring_provider.unwrap_or_default(),
+ allow_insecure_host: value.allow_insecure_host.unwrap_or_default(),
config_setting: value.config_settings.unwrap_or_default(),
no_build_isolation: value.no_build_isolation.unwrap_or_default(),
no_build_isolation_package: value.no_build_isolation_package.unwrap_or_default(),
@@ -1852,6 +1875,7 @@ pub(crate) struct PipSettings {
pub(crate) prefix: Option,
pub(crate) index_strategy: IndexStrategy,
pub(crate) keyring_provider: KeyringProviderType,
+ pub(crate) allow_insecure_host: Vec,
pub(crate) no_build_isolation: bool,
pub(crate) no_build_isolation_package: Vec,
pub(crate) build_options: BuildOptions,
@@ -1906,6 +1930,7 @@ impl PipSettings {
find_links,
index_strategy,
keyring_provider,
+ allow_insecure_host,
no_build,
no_binary,
only_binary,
@@ -1955,6 +1980,7 @@ impl PipSettings {
find_links: top_level_find_links,
index_strategy: top_level_index_strategy,
keyring_provider: top_level_keyring_provider,
+ allow_insecure_host: top_level_allow_insecure_host,
resolution: top_level_resolution,
prerelease: top_level_prerelease,
config_settings: top_level_config_settings,
@@ -1984,6 +2010,7 @@ impl PipSettings {
let find_links = find_links.combine(top_level_find_links);
let index_strategy = index_strategy.combine(top_level_index_strategy);
let keyring_provider = keyring_provider.combine(top_level_keyring_provider);
+ let allow_insecure_host = allow_insecure_host.combine(top_level_allow_insecure_host);
let resolution = resolution.combine(top_level_resolution);
let prerelease = prerelease.combine(top_level_prerelease);
let config_settings = config_settings.combine(top_level_config_settings);
@@ -2043,6 +2070,10 @@ impl PipSettings {
.keyring_provider
.combine(keyring_provider)
.unwrap_or_default(),
+ allow_insecure_host: args
+ .allow_insecure_host
+ .combine(allow_insecure_host)
+ .unwrap_or_default(),
generate_hashes: args
.generate_hashes
.combine(generate_hashes)
@@ -2156,6 +2187,7 @@ impl<'a> From> for ResolverSettingsRef<'a> {
index_locations: settings.index_locations,
index_strategy: settings.index_strategy,
keyring_provider: settings.keyring_provider,
+ allow_insecure_host: settings.allow_insecure_host,
resolution: settings.resolution,
prerelease: settings.prerelease,
config_setting: settings.config_setting,
@@ -2176,6 +2208,7 @@ impl<'a> From> for InstallerSettingsRef<'a> {
index_locations: settings.index_locations,
index_strategy: settings.index_strategy,
keyring_provider: settings.keyring_provider,
+ allow_insecure_host: settings.allow_insecure_host,
config_setting: settings.config_setting,
no_build_isolation: settings.no_build_isolation,
no_build_isolation_package: settings.no_build_isolation_package,
diff --git a/crates/uv/tests/show_settings.rs b/crates/uv/tests/show_settings.rs
index ec73a186a..d6c4fd8ae 100644
--- a/crates/uv/tests/show_settings.rs
+++ b/crates/uv/tests/show_settings.rs
@@ -128,6 +128,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
prefix: None,
index_strategy: FirstIndex,
keyring_provider: Disabled,
+ allow_insecure_host: [],
no_build_isolation: false,
no_build_isolation_package: [],
build_options: BuildOptions {
@@ -266,6 +267,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
prefix: None,
index_strategy: FirstIndex,
keyring_provider: Disabled,
+ allow_insecure_host: [],
no_build_isolation: false,
no_build_isolation_package: [],
build_options: BuildOptions {
@@ -405,6 +407,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
prefix: None,
index_strategy: FirstIndex,
keyring_provider: Disabled,
+ allow_insecure_host: [],
no_build_isolation: false,
no_build_isolation_package: [],
build_options: BuildOptions {
@@ -576,6 +579,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
prefix: None,
index_strategy: FirstIndex,
keyring_provider: Disabled,
+ allow_insecure_host: [],
no_build_isolation: false,
no_build_isolation_package: [],
build_options: BuildOptions {
@@ -693,6 +697,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
prefix: None,
index_strategy: FirstIndex,
keyring_provider: Disabled,
+ allow_insecure_host: [],
no_build_isolation: false,
no_build_isolation_package: [],
build_options: BuildOptions {
@@ -842,6 +847,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
prefix: None,
index_strategy: FirstIndex,
keyring_provider: Disabled,
+ allow_insecure_host: [],
no_build_isolation: false,
no_build_isolation_package: [],
build_options: BuildOptions {
@@ -1028,6 +1034,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
prefix: None,
index_strategy: FirstIndex,
keyring_provider: Disabled,
+ allow_insecure_host: [],
no_build_isolation: false,
no_build_isolation_package: [],
build_options: BuildOptions {
@@ -1213,6 +1220,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
prefix: None,
index_strategy: FirstIndex,
keyring_provider: Disabled,
+ allow_insecure_host: [],
no_build_isolation: false,
no_build_isolation_package: [],
build_options: BuildOptions {
@@ -1376,6 +1384,7 @@ fn resolve_find_links() -> anyhow::Result<()> {
prefix: None,
index_strategy: FirstIndex,
keyring_provider: Disabled,
+ allow_insecure_host: [],
no_build_isolation: false,
no_build_isolation_package: [],
build_options: BuildOptions {
@@ -1515,6 +1524,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
prefix: None,
index_strategy: FirstIndex,
keyring_provider: Disabled,
+ allow_insecure_host: [],
no_build_isolation: false,
no_build_isolation_package: [],
build_options: BuildOptions {
@@ -1692,6 +1702,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
prefix: None,
index_strategy: FirstIndex,
keyring_provider: Disabled,
+ allow_insecure_host: [],
no_build_isolation: false,
no_build_isolation_package: [],
build_options: BuildOptions {
@@ -1852,6 +1863,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
prefix: None,
index_strategy: FirstIndex,
keyring_provider: Disabled,
+ allow_insecure_host: [],
no_build_isolation: false,
no_build_isolation_package: [],
build_options: BuildOptions {
@@ -1991,6 +2003,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
prefix: None,
index_strategy: FirstIndex,
keyring_provider: Disabled,
+ allow_insecure_host: [],
no_build_isolation: false,
no_build_isolation_package: [],
build_options: BuildOptions {
@@ -2113,6 +2126,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
prefix: None,
index_strategy: FirstIndex,
keyring_provider: Disabled,
+ allow_insecure_host: [],
no_build_isolation: false,
no_build_isolation_package: [],
build_options: BuildOptions {
@@ -2235,6 +2249,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
prefix: None,
index_strategy: FirstIndex,
keyring_provider: Disabled,
+ allow_insecure_host: [],
no_build_isolation: false,
no_build_isolation_package: [],
build_options: BuildOptions {
@@ -2359,6 +2374,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
prefix: None,
index_strategy: FirstIndex,
keyring_provider: Disabled,
+ allow_insecure_host: [],
no_build_isolation: false,
no_build_isolation_package: [],
build_options: BuildOptions {
@@ -2488,6 +2504,7 @@ fn resolve_tool() -> anyhow::Result<()> {
find_links: None,
index_strategy: None,
keyring_provider: None,
+ allow_insecure_host: None,
resolution: Some(
LowestDirect,
),
@@ -2523,6 +2540,7 @@ fn resolve_tool() -> anyhow::Result<()> {
},
index_strategy: FirstIndex,
keyring_provider: Disabled,
+ allow_insecure_host: [],
resolution: LowestDirect,
prerelease: IfNecessaryOrExplicit,
config_setting: ConfigSettings(
@@ -2653,6 +2671,7 @@ fn resolve_poetry_toml() -> anyhow::Result<()> {
prefix: None,
index_strategy: FirstIndex,
keyring_provider: Disabled,
+ allow_insecure_host: [],
no_build_isolation: false,
no_build_isolation_package: [],
build_options: BuildOptions {
@@ -2826,6 +2845,7 @@ fn resolve_both() -> anyhow::Result<()> {
prefix: None,
index_strategy: FirstIndex,
keyring_provider: Disabled,
+ allow_insecure_host: [],
no_build_isolation: false,
no_build_isolation_package: [],
build_options: BuildOptions {
@@ -2991,6 +3011,7 @@ fn resolve_config_file() -> anyhow::Result<()> {
prefix: None,
index_strategy: FirstIndex,
keyring_provider: Disabled,
+ allow_insecure_host: [],
no_build_isolation: false,
no_build_isolation_package: [],
build_options: BuildOptions {
@@ -3208,6 +3229,7 @@ fn resolve_skip_empty() -> anyhow::Result<()> {
prefix: None,
index_strategy: FirstIndex,
keyring_provider: Disabled,
+ allow_insecure_host: [],
no_build_isolation: false,
no_build_isolation_package: [],
build_options: BuildOptions {
@@ -3333,6 +3355,7 @@ fn resolve_skip_empty() -> anyhow::Result<()> {
prefix: None,
index_strategy: FirstIndex,
keyring_provider: Disabled,
+ allow_insecure_host: [],
no_build_isolation: false,
no_build_isolation_package: [],
build_options: BuildOptions {
diff --git a/docs/configuration/authentication.md b/docs/configuration/authentication.md
index 637c2568c..5aed34eb9 100644
--- a/docs/configuration/authentication.md
+++ b/docs/configuration/authentication.md
@@ -74,6 +74,23 @@ If client certificate authentication (mTLS) is desired, set the `SSL_CLIENT_CERT
variable to the path of the PEM formatted file containing the certificate followed by the private
key.
+Finally, if you're using a setup in which you want to trust a self-signed certificate or otherwise
+disable certificate verification, you can instruct uv to allow insecure connections to dedicated
+hosts via the `allow-insecure-host` configuration option. For example, adding the following to
+`pyproject.toml` will allow insecure connections to `example.com`:
+
+```toml
+[tool.uv]
+allow-insecure-host = ["example.com"]
+```
+
+`allow-insecure-host` expects to receive a hostname (e.g., `localhost`) or hostname-port pair (e.g.,
+`localhost:8080`), and is only applicable to HTTPS connections, as HTTP connections are inherently
+insecure.
+
+Use `allow-insecure-host` with caution and only in trusted environments, as it can expose you to
+security risks due to the lack of certificate verification.
+
## Authentication with alternative package indexes
See the [alternative indexes integration guide](../guides/integration/alternative-indexes.md) for
diff --git a/docs/reference/cli.md b/docs/reference/cli.md
index a6ed6c525..7d9644f6f 100644
--- a/docs/reference/cli.md
+++ b/docs/reference/cli.md
@@ -70,6 +70,14 @@ uv run [OPTIONS]
This option is only available when running in a project.
+--allow-insecure-host allow-insecure-hostAllow insecure connections to a host.
+
+Can be provided multiple times.
+
+Expects to receive either a hostname (e.g., localhost), a host-port pair (e.g., localhost:8080), or a URL (e.g., https://localhost).
+
+WARNING: Hosts included in this list will not be verified against the system’s certificate store. Only use --allow-insecure-host in a secure network with verified sources, as it bypasses SSL verification and could expose you to MITM attacks.
+
--cache-dir cache-dirPath to the cache directory.
Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.
@@ -483,7 +491,15 @@ uv add [OPTIONS] >
Options
---branch branchBranch to use when adding a dependency from Git
+--allow-insecure-host allow-insecure-hostAllow insecure connections to a host.
+
+Can be provided multiple times.
+
+Expects to receive either a hostname (e.g., localhost), a host-port pair (e.g., localhost:8080), or a URL (e.g., https://localhost).
+
+WARNING: Hosts included in this list will not be verified against the system’s certificate store. Only use --allow-insecure-host in a secure network with verified sources, as it bypasses SSL verification and could expose you to MITM attacks.
+
+--branch branchBranch to use when adding a dependency from Git
--cache-dir cache-dirPath to the cache directory.
@@ -767,7 +783,15 @@ uv remove [OPTIONS] ...
Options
---cache-dir cache-dirPath to the cache directory.
+--allow-insecure-host allow-insecure-hostAllow insecure connections to a host.
+
+Can be provided multiple times.
+
+Expects to receive either a hostname (e.g., localhost), a host-port pair (e.g., localhost:8080), or a URL (e.g., https://localhost).
+
+WARNING: Hosts included in this list will not be verified against the system’s certificate store. Only use --allow-insecure-host in a secure network with verified sources, as it bypasses SSL verification and could expose you to MITM attacks.
+
+--cache-dir cache-dirPath to the cache directory.
Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.
@@ -1023,6 +1047,14 @@ uv sync [OPTIONS]
--all-extrasInclude all optional dependencies
+--allow-insecure-host allow-insecure-hostAllow insecure connections to a host.
+
+Can be provided multiple times.
+
+Expects to receive either a hostname (e.g., localhost), a host-port pair (e.g., localhost:8080), or a URL (e.g., https://localhost).
+
+WARNING: Hosts included in this list will not be verified against the system’s certificate store. Only use --allow-insecure-host in a secure network with verified sources, as it bypasses SSL verification and could expose you to MITM attacks.
+
--cache-dir cache-dirPath to the cache directory.
Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.
@@ -1289,7 +1321,15 @@ uv lock [OPTIONS]
Options
---cache-dir cache-dirPath to the cache directory.
+--allow-insecure-host allow-insecure-hostAllow insecure connections to a host.
+
+Can be provided multiple times.
+
+Expects to receive either a hostname (e.g., localhost), a host-port pair (e.g., localhost:8080), or a URL (e.g., https://localhost).
+
+WARNING: Hosts included in this list will not be verified against the system’s certificate store. Only use --allow-insecure-host in a secure network with verified sources, as it bypasses SSL verification and could expose you to MITM attacks.
+
+--cache-dir cache-dirPath to the cache directory.
Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.
@@ -1513,7 +1553,15 @@ uv tree [OPTIONS]
Options
---cache-dir cache-dirPath to the cache directory.
+--allow-insecure-host allow-insecure-hostAllow insecure connections to a host.
+
+Can be provided multiple times.
+
+Expects to receive either a hostname (e.g., localhost), a host-port pair (e.g., localhost:8080), or a URL (e.g., https://localhost).
+
+WARNING: Hosts included in this list will not be verified against the system’s certificate store. Only use --allow-insecure-host in a secure network with verified sources, as it bypasses SSL verification and could expose you to MITM attacks.
+
+--cache-dir cache-dirPath to the cache directory.
Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.
@@ -1839,7 +1887,15 @@ uv tool run [OPTIONS] [COMMAND]
Options
---cache-dir cache-dirPath to the cache directory.
+--allow-insecure-host allow-insecure-hostAllow insecure connections to a host.
+
+Can be provided multiple times.
+
+Expects to receive either a hostname (e.g., localhost), a host-port pair (e.g., localhost:8080), or a URL (e.g., https://localhost).
+
+WARNING: Hosts included in this list will not be verified against the system’s certificate store. Only use --allow-insecure-host in a secure network with verified sources, as it bypasses SSL verification and could expose you to MITM attacks.
+
+--cache-dir cache-dirPath to the cache directory.
Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.
@@ -2081,7 +2137,15 @@ uv tool install [OPTIONS]
Options
---cache-dir cache-dirPath to the cache directory.
+--allow-insecure-host allow-insecure-hostAllow insecure connections to a host.
+
+Can be provided multiple times.
+
+Expects to receive either a hostname (e.g., localhost), a host-port pair (e.g., localhost:8080), or a URL (e.g., https://localhost).
+
+WARNING: Hosts included in this list will not be verified against the system’s certificate store. Only use --allow-insecure-host in a secure network with verified sources, as it bypasses SSL verification and could expose you to MITM attacks.
+
+--cache-dir cache-dirPath to the cache directory.
Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.
@@ -2323,6 +2387,14 @@ uv tool upgrade [OPTIONS]
--allUpgrade all tools
+--allow-insecure-host allow-insecure-hostAllow insecure connections to a host.
+
+Can be provided multiple times.
+
+Expects to receive either a hostname (e.g., localhost), a host-port pair (e.g., localhost:8080), or a URL (e.g., https://localhost).
+
+WARNING: Hosts included in this list will not be verified against the system’s certificate store. Only use --allow-insecure-host in a secure network with verified sources, as it bypasses SSL verification and could expose you to MITM attacks.
+
--cache-dir cache-dirPath to the cache directory.
Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.
@@ -3586,6 +3658,14 @@ uv pip compile [OPTIONS] ...
Only applies to pyproject.toml, setup.py, and setup.cfg sources.
+--allow-insecure-host allow-insecure-hostAllow insecure connections to a host.
+
+Can be provided multiple times.
+
+Expects to receive either a hostname (e.g., localhost), a host-port pair (e.g., localhost:8080), or a URL (e.g., https://localhost).
+
+WARNING: Hosts included in this list will not be verified against the system’s certificate store. Only use --allow-insecure-host in a secure network with verified sources, as it bypasses SSL verification and could expose you to MITM attacks.
+
--annotation-style annotation-styleThe style of the annotation comments included in the output file, used to indicate the source of each package.
Defaults to split.
@@ -3944,6 +4024,14 @@ uv pip sync [OPTIONS] ...
--allow-empty-requirementsAllow sync of empty requirements, which will clear the environment of all packages
+--allow-insecure-host allow-insecure-hostAllow insecure connections to a host.
+
+Can be provided multiple times.
+
+Expects to receive either a hostname (e.g., localhost), a host-port pair (e.g., localhost:8080), or a URL (e.g., https://localhost).
+
+WARNING: Hosts included in this list will not be verified against the system’s certificate store. Only use --allow-insecure-host in a secure network with verified sources, as it bypasses SSL verification and could expose you to MITM attacks.
+
--break-system-packagesAllow uv to modify an EXTERNALLY-MANAGED Python installation.
WARNING: --break-system-packages is intended for use in continuous integration (CI) environments, when installing into Python installations that are managed by an external package manager, like apt. It should be used with caution, as such Python installations explicitly recommend against modifications by other package managers (like uv or pip).
@@ -4235,6 +4323,14 @@ uv pip install [OPTIONS] |--editable Only applies to pyproject.toml, setup.py, and setup.cfg sources.
+--allow-insecure-host allow-insecure-hostAllow insecure connections to a host.
+
+Can be provided multiple times.
+
+Expects to receive either a hostname (e.g., localhost), a host-port pair (e.g., localhost:8080), or a URL (e.g., https://localhost).
+
+WARNING: Hosts included in this list will not be verified against the system’s certificate store. Only use --allow-insecure-host in a secure network with verified sources, as it bypasses SSL verification and could expose you to MITM attacks.
+
--break-system-packagesAllow uv to modify an EXTERNALLY-MANAGED Python installation.
WARNING: --break-system-packages is intended for use in continuous integration (CI) environments, when installing into Python installations that are managed by an external package manager, like apt. It should be used with caution, as such Python installations explicitly recommend against modifications by other package managers (like uv or pip).
@@ -4582,7 +4678,15 @@ uv pip uninstall [OPTIONS] >
Options
---break-system-packagesAllow uv to modify an EXTERNALLY-MANAGED Python installation.
+--allow-insecure-host allow-insecure-hostAllow insecure connections to a host.
+
+Can be provided multiple times.
+
+Expects to receive either a hostname (e.g., localhost), a host-port pair (e.g., localhost:8080), or a URL (e.g., https://localhost).
+
+WARNING: Hosts included in this list will not be verified against the system’s certificate store. Only use --allow-insecure-host in a secure network with verified sources, as it bypasses SSL verification and could expose you to MITM attacks.
+
+--break-system-packagesAllow uv to modify an EXTERNALLY-MANAGED Python installation.
WARNING: --break-system-packages is intended for use in continuous integration (CI) environments, when installing into Python installations that are managed by an external package manager, like apt. It should be used with caution, as such Python installations explicitly recommend against modifications by other package managers (like uv or pip).
@@ -5228,6 +5332,14 @@ uv venv [OPTIONS] [NAME]
WARNING: This option can lead to unexpected behavior if the existing virtual environment and the newly-created virtual environment are linked to different Python interpreters.
+--allow-insecure-host allow-insecure-hostAllow insecure connections to a host.
+
+Can be provided multiple times.
+
+Expects to receive either a hostname (e.g., localhost), a host-port pair (e.g., localhost:8080), or a URL (e.g., https://localhost).
+
+WARNING: Hosts included in this list will not be verified against the system’s certificate store. Only use --allow-insecure-host in a secure network with verified sources, as it bypasses SSL verification and could expose you to MITM attacks.
+
--cache-dir cache-dirPath to the cache directory.
Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.
diff --git a/docs/reference/settings.md b/docs/reference/settings.md
index d2141ef44..fc9b11a62 100644
--- a/docs/reference/settings.md
+++ b/docs/reference/settings.md
@@ -1,4 +1,36 @@
## Global
+#### [`allow-insecure-host`](#allow-insecure-host) {: #allow-insecure-host }
+
+Allow insecure connections to host.
+
+Expects to receive either a hostname (e.g., `localhost`), a host-port pair (e.g.,
+`localhost:8080`), or a URL (e.g., `https://localhost`).
+
+WARNING: Hosts included in this list will not be verified against the system's certificate
+store. Only use `--allow-insecure-host` in a secure network with verified sources, as it
+bypasses SSL verification and could expose you to MITM attacks.
+
+**Default value**: `[]`
+
+**Type**: `list[str]`
+
+**Example usage**:
+
+=== "pyproject.toml"
+
+ ```toml
+ [tool.uv]
+ allow-insecure-host = ["localhost:8080"]
+ ```
+=== "uv.toml"
+
+ ```toml
+
+ allow-insecure-host = ["localhost:8080"]
+ ```
+
+---
+
#### [`cache-dir`](#cache-dir) {: #cache-dir }
Path to the cache directory.
@@ -1176,6 +1208,39 @@ packages.
---
+#### [`allow-insecure-host`](#pip_allow-insecure-host) {: #pip_allow-insecure-host }
+
+
+Allow insecure connections to host.
+
+Expects to receive either a hostname (e.g., `localhost`), a host-port pair (e.g.,
+`localhost:8080`), or a URL (e.g., `https://localhost`).
+
+WARNING: Hosts included in this list will not be verified against the system's certificate
+store. Only use `--allow-insecure-host` in a secure network with verified sources, as it
+bypasses SSL verification and could expose you to MITM attacks.
+
+**Default value**: `[]`
+
+**Type**: `list[str]`
+
+**Example usage**:
+
+=== "pyproject.toml"
+
+ ```toml
+ [tool.uv.pip]
+ allow-insecure-host = ["localhost:8080"]
+ ```
+=== "uv.toml"
+
+ ```toml
+ [pip]
+ allow-insecure-host = ["localhost:8080"]
+ ```
+
+---
+
#### [`annotation-style`](#pip_annotation-style) {: #pip_annotation-style }
diff --git a/uv.schema.json b/uv.schema.json
index de3a90047..2ba30d6e9 100644
--- a/uv.schema.json
+++ b/uv.schema.json
@@ -4,6 +4,16 @@
"description": "Metadata and configuration for uv.",
"type": "object",
"properties": {
+ "allow-insecure-host": {
+ "description": "Allow insecure connections to host.\n\nExpects to receive either a hostname (e.g., `localhost`), a host-port pair (e.g., `localhost:8080`), or a URL (e.g., `https://localhost`).\n\nWARNING: Hosts included in this list will not be verified against the system's certificate store. Only use `--allow-insecure-host` in a secure network with verified sources, as it bypasses SSL verification and could expose you to MITM attacks.",
+ "type": [
+ "array",
+ "null"
+ ],
+ "items": {
+ "$ref": "#/definitions/TrustedHost"
+ }
+ },
"cache-dir": {
"description": "Path to the cache directory.\n\nDefaults to `$HOME/Library/Caches/uv` on macOS, `$XDG_CACHE_HOME/uv` or `$HOME/.cache/uv` on Linux, and `%LOCALAPPDATA%\\uv\\cache` on Windows.",
"type": [
@@ -561,6 +571,16 @@
"null"
]
},
+ "allow-insecure-host": {
+ "description": "Allow insecure connections to host.\n\nExpects to receive either a hostname (e.g., `localhost`), a host-port pair (e.g., `localhost:8080`), or a URL (e.g., `https://localhost`).\n\nWARNING: Hosts included in this list will not be verified against the system's certificate store. Only use `--allow-insecure-host` in a secure network with verified sources, as it bypasses SSL verification and could expose you to MITM attacks.",
+ "type": [
+ "array",
+ "null"
+ ],
+ "items": {
+ "$ref": "#/definitions/TrustedHost"
+ }
+ },
"annotation-style": {
"description": "The style of the annotation comments included in the output file, used to indicate the source of each package.",
"anyOf": [
@@ -1426,6 +1446,10 @@
}
},
"additionalProperties": false
+ },
+ "TrustedHost": {
+ "description": "A host or host-port pair.",
+ "type": "string"
}
}
}
\ No newline at end of file