Monorepo for Tangled tangled.org
3

Configure Feed

Select the types of activity you want to include in your feed.

bobbin extra certs

Signed-off-by: oppiliappan <me@oppi.li>

author
oppiliappan
date (Jul 27, 2026, 12:53 PM +0100) commit d391344e parent 79d1c35f change-id nzoqnvmx
+93 -18
+8
bobbin/crates/bobbin/src/config.rs
··· 27 27 "search.heap_bytes", 28 28 "knot.allow_private", 29 29 "knot.require_https", 30 + "knot.extra_ca_cert", 30 31 "log.format", 31 32 "log.filter", 32 33 ]; ··· 49 50 "BOBBIN_SEARCH_HEAP_BYTES", 50 51 "BOBBIN_KNOT_ALLOW_PRIVATE", 51 52 "BOBBIN_KNOT_REQUIRE_HTTPS", 53 + "BOBBIN_KNOT_EXTRA_CA_CERT", 52 54 "BOBBIN_LOG_FORMAT", 53 55 "BOBBIN_LOG", 54 56 ]; ··· 223 225 /// knot for development. 224 226 #[config(env = "BOBBIN_KNOT_REQUIRE_HTTPS", default = true)] 225 227 pub require_https: bool, 228 + 229 + /// Path to an extra PEM certificate to trust as a knot CA, in addition to 230 + /// the bundled webpki roots. Set for local dev so a self-signed/dev CA is 231 + /// trusted without disabling certificate verification. Empty disables this. 232 + #[config(env = "BOBBIN_KNOT_EXTRA_CA_CERT", default = "")] 233 + pub extra_ca_cert: String, 226 234 } 227 235 228 236 #[derive(Debug, Config)]
+5 -1
bobbin/crates/bobbin/src/main.rs
··· 191 191 let coverage = Arc::new(CoverageWatch::new()); 192 192 let warming_buffer = Arc::new(WarmingBuffer::new(hasher.clone())); 193 193 let knot_registry = Arc::new(KnotRegistry::new()); 194 + let knot_extra_ca_cert: Option<PathBuf> = (!cfg.knot.extra_ca_cert.is_empty()) 195 + .then(|| PathBuf::from(&cfg.knot.extra_ca_cert)); 194 196 let knots = Arc::new(KnotProxy::new( 195 197 KnotProxyConfig { 196 198 allow_private_hosts: cfg.knot.allow_private, 197 199 require_https: cfg.knot.require_https, 200 + extra_ca_cert: knot_extra_ca_cert.clone(), 198 201 ..KnotProxyConfig::default() 199 202 }, 200 203 KnotHttpConfig::default(), ··· 223 226 224 227 let knot_acl_dev = !cfg.knot.require_https; 225 228 let knot_allow_private = cfg.knot.allow_private; 226 - let knot_client = KnotClient::with_default_http(knot_allow_private)?; 229 + let knot_client = 230 + KnotClient::with_default_http(knot_allow_private, knot_extra_ca_cert.as_deref())?; 227 231 let knot_gate = Arc::new(CapabilityGate::new( 228 232 knot_client.clone(), 229 233 clock.clone(),
+1 -1
bobbin/crates/ingest/src/lib.rs
··· 1707 1707 let native_host = format!("{}:{}", url.host_str().unwrap(), url.port().unwrap()); 1708 1708 1709 1709 let gate = CapabilityGate::new( 1710 - KnotClient::with_default_http(true).unwrap(), 1710 + KnotClient::with_default_http(true, None).unwrap(), 1711 1711 Arc::new(SystemClock::new()), 1712 1712 true, 1713 1713 true,
+19 -7
bobbin/crates/knot-ingest/src/client.rs
··· 72 72 BodyTooLarge { limit: u64 }, 73 73 #[error("decode response: {0}")] 74 74 Decode(#[from] serde_json::Error), 75 + #[error("extra ca cert: {0}")] 76 + ExtraCa(#[from] bobbin_runtime::ExtraCaError), 75 77 } 76 78 77 79 pub fn knot_endpoint( ··· 94 96 Ok(knot) 95 97 } 96 98 97 - fn default_http_client(allow_private: bool) -> Result<reqwest::Client, reqwest::Error> { 98 - reqwest::Client::builder() 99 + fn default_http_client( 100 + allow_private: bool, 101 + extra_ca_cert: Option<&std::path::Path>, 102 + ) -> Result<reqwest::Client, KnotClientError> { 103 + let mut builder = reqwest::Client::builder() 99 104 .user_agent(USER_AGENT) 100 105 .timeout(REQUEST_TIMEOUT) 101 106 .connect_timeout(CONNECT_TIMEOUT) 102 107 .redirect(reqwest::redirect::Policy::none()) 103 - .dns_resolver(Arc::new(PrivateAddressFilter::new(allow_private))) 108 + .dns_resolver(Arc::new(PrivateAddressFilter::new(allow_private))); 109 + if let Some(path) = extra_ca_cert { 110 + builder = builder.add_root_certificate(bobbin_runtime::load_extra_ca_cert(path)?); 111 + } 112 + builder 104 113 .build() 114 + .map_err(|e| KnotClientError::Build(e.to_string())) 105 115 } 106 116 107 117 fn nsid(s: &'static str) -> Nsid<DefaultStr> { ··· 122 132 Self { http } 123 133 } 124 134 125 - pub fn with_default_http(allow_private: bool) -> Result<Self, KnotClientError> { 126 - let client = default_http_client(allow_private) 127 - .map_err(|e| KnotClientError::Build(e.to_string()))?; 135 + pub fn with_default_http( 136 + allow_private: bool, 137 + extra_ca_cert: Option<&std::path::Path>, 138 + ) -> Result<Self, KnotClientError> { 139 + let client = default_http_client(allow_private, extra_ca_cert)?; 128 140 Ok(Self::new(ReqwestHttp::shared(client))) 129 141 } 130 142 ··· 282 294 } 283 295 284 296 fn client() -> KnotClient { 285 - KnotClient::new(ReqwestHttp::shared(default_http_client(true).unwrap())) 297 + KnotClient::new(ReqwestHttp::shared(default_http_client(true, None).unwrap())) 286 298 } 287 299 288 300 fn endpoint(server: &MockServer) -> KnotHost {
+23 -6
bobbin/crates/knot-proxy/src/lib.rs
··· 1 + use std::path::PathBuf; 1 2 use std::pin::Pin; 2 3 use std::sync::Arc; 3 4 use std::task::{Context, Poll}; 4 5 use std::time::Duration; 5 6 6 7 use bobbin_runtime::{ 7 - BodyStream as InnerBodyStream, Clock, HttpRequest, HttpResponseHead, HttpTransport, 8 - NetworkError, ReqwestHttp, RuntimeHasher, 8 + BodyStream as InnerBodyStream, Clock, ExtraCaError, HttpRequest, HttpResponseHead, 9 + HttpTransport, NetworkError, ReqwestHttp, RuntimeHasher, load_extra_ca_cert, 9 10 }; 10 11 use bytes::Bytes; 11 12 use futures::Stream; ··· 34 35 pub cooldown: Duration, 35 36 pub allow_private_hosts: bool, 36 37 pub require_https: bool, 38 + /// Extra PEM CA certificate to trust for knot TLS connections, in 39 + /// addition to the bundled webpki roots. For local dev CAs only. 40 + pub extra_ca_cert: Option<PathBuf>, 37 41 } 38 42 39 43 impl Default for KnotProxyConfig { ··· 43 47 cooldown: Duration::from_secs(30), 44 48 allow_private_hosts: false, 45 49 require_https: true, 50 + extra_ca_cert: None, 46 51 } 47 52 } 48 53 } ··· 95 100 clock: Arc<dyn Clock>, 96 101 } 97 102 103 + #[derive(Debug, Error)] 104 + pub enum KnotProxyBuildError { 105 + #[error("extra ca cert: {0}")] 106 + ExtraCa(#[from] ExtraCaError), 107 + #[error("build http client: {0}")] 108 + Client(#[from] reqwest::Error), 109 + } 110 + 98 111 impl KnotProxy { 99 112 pub fn new( 100 113 config: KnotProxyConfig, 101 114 http: KnotHttpConfig, 102 115 clock: Arc<dyn Clock>, 103 116 hasher: RuntimeHasher, 104 - ) -> Result<Self, reqwest::Error> { 117 + ) -> Result<Self, KnotProxyBuildError> { 105 118 let resolver = Arc::new(dns::PrivateAddressFilter::new(config.allow_private_hosts)); 106 - let client = Client::builder() 119 + let mut builder = Client::builder() 107 120 .user_agent(USER_AGENT) 108 121 .connect_timeout(http.connect_timeout) 109 122 .read_timeout(http.read_timeout) ··· 111 124 .no_gzip() 112 125 .no_brotli() 113 126 .no_deflate() 114 - .dns_resolver(resolver) 115 - .build()?; 127 + .dns_resolver(resolver); 128 + if let Some(path) = &config.extra_ca_cert { 129 + builder = builder.add_root_certificate(load_extra_ca_cert(path)?); 130 + } 131 + let client = builder.build()?; 116 132 Ok(Self::with_transport( 117 133 ReqwestHttp::shared(client), 118 134 config, ··· 347 363 cooldown: Duration::from_millis(80), 348 364 allow_private_hosts: true, 349 365 require_https: false, 366 + extra_ca_cert: None, 350 367 } 351 368 } 352 369
+4 -3
bobbin/crates/runtime/src/lib.rs
··· 14 14 MemWsResponder, MemWsServerFuture, MemWsTransport, 15 15 }; 16 16 pub use network::{ 17 - AddrGuard, BodyStream, GuardedWs, HttpRequest, HttpResponseFuture, HttpResponseHead, 18 - HttpResult, HttpTransport, NetworkError, ReqwestHttp, TungsteniteWs, WsConn, WsConnectFuture, 19 - WsMessage, WsMessageFuture, WsSendFuture, WsSink, WsStream, WsTransport, 17 + AddrGuard, BodyStream, ExtraCaError, GuardedWs, HttpRequest, HttpResponseFuture, 18 + HttpResponseHead, HttpResult, HttpTransport, NetworkError, ReqwestHttp, TungsteniteWs, WsConn, 19 + WsConnectFuture, WsMessage, WsMessageFuture, WsSendFuture, WsSink, WsStream, WsTransport, 20 + load_extra_ca_cert, 20 21 };
+27
bobbin/crates/runtime/src/network.rs
··· 65 65 } 66 66 } 67 67 68 + #[derive(Debug, Error)] 69 + pub enum ExtraCaError { 70 + #[error("read {path}: {source}")] 71 + Read { 72 + path: String, 73 + source: std::io::Error, 74 + }, 75 + #[error("parse PEM certificate at {path}: {source}")] 76 + Parse { path: String, source: reqwest::Error }, 77 + } 78 + 79 + /// Loads a PEM certificate from `path` (if given) so it can be added to a 80 + /// `reqwest::ClientBuilder` via `add_root_certificate`, extending the bundled 81 + /// webpki roots rather than replacing them. Used to trust a local dev CA - 82 + /// `rustls-tls-webpki-roots` ignores the OS trust store, so mounting a CA 83 + /// into `/etc/ssl/certs` has no effect on reqwest without this. 84 + pub fn load_extra_ca_cert(path: &std::path::Path) -> Result<reqwest::Certificate, ExtraCaError> { 85 + let pem = std::fs::read(path).map_err(|source| ExtraCaError::Read { 86 + path: path.display().to_string(), 87 + source, 88 + })?; 89 + reqwest::Certificate::from_pem(&pem).map_err(|source| ExtraCaError::Parse { 90 + path: path.display().to_string(), 91 + source, 92 + }) 93 + } 94 + 68 95 impl HttpTransport for ReqwestHttp { 69 96 fn execute(&self, request: HttpRequest) -> HttpResponseFuture { 70 97 let client = self.client.clone();
+1
bobbin/crates/xrpc/tests/knot_proxy.rs
··· 37 37 cooldown: Duration::from_millis(80), 38 38 allow_private_hosts: true, 39 39 require_https: false, 40 + extra_ca_cert: None, 40 41 } 41 42 } 42 43
+1
docker-compose.yml
··· 424 424 BOBBIN_SLINGSHOT_URL: http://hydrant:3000 425 425 BOBBIN_KNOT_ALLOW_PRIVATE: "true" 426 426 BOBBIN_KNOT_REQUIRE_HTTPS: "false" 427 + BOBBIN_KNOT_EXTRA_CA_CERT: /etc/ssl/certs/ca-certificates.crt 427 428 BOBBIN_LOG: info 428 429 volumes: 429 430 - .:/src:cached
+4
localinfra/Caddyfile
··· 32 32 } 33 33 34 34 # knot 35 + http://knot.tngl.boltless.dev { 36 + reverse_proxy knot:5555 37 + } 38 + 35 39 knot.tngl.boltless.dev { 36 40 tls internal 37 41 reverse_proxy knot:5555