From d2a8342e7cf3bb597116d4ea41db4d7a5ac17b17 Mon Sep 17 00:00:00 2001 From: Joe Sylve Date: Tue, 30 Jun 2026 00:00:00 +0000 Subject: [PATCH] feat(oci-client): use plain HTTP for loopback registries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `OciClientDriver::get_metadata` builds its oci-client with `ClientConfig::default()`, whose protocol is HTTPS. The CLI also links reqwest with `rustls-tls` (webpki roots) and hardcodes `accept_invalid_certificates: false`, so a self-signed TLS registry is not trusted either. The result: `bluebuild build` cannot inspect an image on a plain-HTTP local registry — the post-push manifest fetch fails with a connect error against `https://localhost:5000/...`. Treat loopback registries (localhost, 127.0.0.0/8, ::1 — any port) as plain HTTP, exactly as cosign, containerd, and docker already do. Everything else stays HTTPS, so there is no behavior change for real registries and no new configuration to set. The loopback-host detection is factored into a `is_loopback_registry` helper (handling optional ports and bracketed IPv6 literals) with unit tests. Signed-off-by: Joe Sylve --- process/drivers/oci_client_driver.rs | 76 +++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/process/drivers/oci_client_driver.rs b/process/drivers/oci_client_driver.rs index 42bfedd2..b5f1c726 100644 --- a/process/drivers/oci_client_driver.rs +++ b/process/drivers/oci_client_driver.rs @@ -17,7 +17,18 @@ impl InspectDriver for OciClientDriver { fn get_metadata(opts: GetMetadataOpts) -> Result { #[cached(result = true, key = "String", convert = r"{image.to_string()}")] fn inner(image: &Reference) -> Result { - let client = oci_client::Client::new(ClientConfig::default()); + // Speak plain HTTP to loopback registries (localhost, 127.0.0.0/8, + // ::1 — any port), matching the convention of cosign, containerd, + // and docker. Everything else stays HTTPS. No configuration needed. + let protocol = if is_loopback_registry(image.registry()) { + oci_client::client::ClientProtocol::Http + } else { + oci_client::client::ClientProtocol::default() + }; + let client = oci_client::Client::new(ClientConfig { + protocol, + ..ClientConfig::default() + }); let auth = match Credentials::get(image.registry()) { Some(Credentials::Basic { username, password }) => { RegistryAuth::Basic(username, password.value().into()) @@ -81,3 +92,66 @@ impl InspectDriver for OciClientDriver { } } } + +/// Returns `true` when `registry` refers to a loopback host — `localhost`, an +/// IPv4 address in `127.0.0.0/8`, or `::1` — optionally with a `:port` suffix +/// and `[...]` brackets around an IPv6 literal. +/// +/// Loopback registries are served over plain HTTP (matching cosign, +/// containerd, and docker); every other registry uses HTTPS. +fn is_loopback_registry(registry: &str) -> bool { + // Strip an optional `:port` suffix, but leave an unbracketed IPv6 literal + // (which itself contains `:`) intact. + let host = registry + .rsplit_once(':') + .filter(|(h, _)| !h.contains(':') || h.ends_with(']')) + .map_or(registry, |(h, _)| h); + let host = host.trim_start_matches('[').trim_end_matches(']'); + host == "localhost" + || host + .parse::() + .is_ok_and(|ip| ip.is_loopback()) +} + +#[cfg(test)] +mod test { + use super::is_loopback_registry; + + #[test] + fn loopback_registries_use_http() { + for registry in [ + "localhost", + "localhost:5000", + "127.0.0.1", + "127.0.0.1:5000", + "127.5.6.7", // anywhere in 127.0.0.0/8 + "::1", + "[::1]", + "[::1]:5000", + ] { + assert!( + is_loopback_registry(registry), + "{registry} should be treated as loopback" + ); + } + } + + #[test] + fn remote_registries_use_https() { + for registry in [ + "ghcr.io", + "registry.example.com", + "registry.example.com:5000", + "192.168.1.10", + "192.168.1.10:5000", + "8.8.8.8", + "[2001:db8::1]:5000", + "localhost.example.com", // not the loopback host + ] { + assert!( + !is_loopback_registry(registry), + "{registry} should not be treated as loopback" + ); + } + } +}