1use std::sync::Arc;
2
3use http::{HeaderValue, header::HeaderName};
4#[cfg(feature = "openssl-tls")] use hyper::rt::{Read, Write};
5use hyper_util::client::legacy::connect::HttpConnector;
6use jiff::Timestamp;
7use secrecy::ExposeSecret;
8use tower::{filter::AsyncFilterLayer, util::Either};
9
10#[cfg(any(feature = "rustls-tls", feature = "openssl-tls"))] use super::tls;
11use super::{
12 auth::Auth,
13 middleware::{AddAuthorizationLayer, AuthLayer, BaseUriLayer, ExtraHeadersLayer},
14};
15use crate::{Config, Error, Result};
16
17pub trait ConfigExt: private::Sealed {
23 fn base_uri_layer(&self) -> BaseUriLayer;
25
26 fn auth_layer(&self) -> Result<Option<AuthLayer>>;
28
29 fn extra_headers_layer(&self) -> Result<ExtraHeadersLayer>;
31
32 #[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
47 #[cfg(feature = "rustls-tls")]
48 fn rustls_https_connector(&self) -> Result<hyper_rustls::HttpsConnector<HttpConnector>>;
49
50 #[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
67 #[cfg(feature = "rustls-tls")]
68 fn rustls_https_connector_with_connector<H>(
69 &self,
70 connector: H,
71 ) -> Result<hyper_rustls::HttpsConnector<H>>;
72
73 #[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
91 #[cfg(feature = "rustls-tls")]
92 fn rustls_client_config(&self) -> Result<rustls::ClientConfig>;
93
94 #[cfg_attr(docsrs, doc(cfg(feature = "openssl-tls")))]
106 #[cfg(feature = "openssl-tls")]
107 fn openssl_https_connector(&self)
108 -> Result<hyper_openssl::client::legacy::HttpsConnector<HttpConnector>>;
109
110 #[cfg_attr(docsrs, doc(cfg(feature = "openssl-tls")))]
125 #[cfg(feature = "openssl-tls")]
126 fn openssl_https_connector_with_connector<H>(
127 &self,
128 connector: H,
129 ) -> Result<hyper_openssl::client::legacy::HttpsConnector<H>>
130 where
131 H: tower::Service<http::Uri> + Send,
132 H::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
133 H::Future: Send + 'static,
134 H::Response: Read + Write + hyper_util::client::legacy::connect::Connection + Unpin;
135
136 #[cfg_attr(docsrs, doc(cfg(feature = "openssl-tls")))]
154 #[cfg(feature = "openssl-tls")]
155 fn openssl_ssl_connector_builder(&self) -> Result<openssl::ssl::SslConnectorBuilder>;
156}
157
158#[cfg(all(test, feature = "openssl-tls"))]
159mod openssl_tls_server_name_tests {
160 use std::{
161 net::TcpListener,
162 sync::{Arc, Mutex},
163 };
164
165 use openssl::{
166 asn1::Asn1Time,
167 hash::MessageDigest,
168 pkey::{PKey, Private},
169 rsa::Rsa,
170 ssl::{NameType, SslAcceptor, SslMethod},
171 x509::{
172 X509, X509NameBuilder,
173 extension::{BasicConstraints, SubjectAlternativeName},
174 },
175 };
176 use tower::ServiceExt as _;
177
178 use super::*;
179
180 enum San<'a> {
181 Dns(&'a str),
182 Ip(&'a str),
183 }
184
185 fn self_signed_cert(san: San) -> (X509, PKey<Private>) {
188 let cn = match san {
189 San::Dns(s) | San::Ip(s) => s,
190 };
191 let pkey = PKey::from_rsa(Rsa::generate(2048).unwrap()).unwrap();
192
193 let mut name = X509NameBuilder::new().unwrap();
194 name.append_entry_by_text("CN", cn).unwrap();
195 let name = name.build();
196
197 let mut builder = X509::builder().unwrap();
198 builder.set_version(2).unwrap();
199 builder.set_subject_name(&name).unwrap();
200 builder.set_issuer_name(&name).unwrap();
201 builder.set_pubkey(&pkey).unwrap();
202 builder
203 .set_not_before(&Asn1Time::days_from_now(0).unwrap())
204 .unwrap();
205 builder
206 .set_not_after(&Asn1Time::days_from_now(1).unwrap())
207 .unwrap();
208 builder
209 .append_extension(BasicConstraints::new().critical().ca().build().unwrap())
210 .unwrap();
211 let mut san_ext = SubjectAlternativeName::new();
212 match san {
213 San::Dns(s) => san_ext.dns(s),
214 San::Ip(s) => san_ext.ip(s),
215 };
216 let san_ext = san_ext.build(&builder.x509v3_context(None, None)).unwrap();
217 builder.append_extension(san_ext).unwrap();
218 builder.sign(&pkey, MessageDigest::sha256()).unwrap();
219
220 (builder.build(), pkey)
221 }
222
223 fn spawn_tls_server(cert: X509, key: PKey<Private>) -> (u16, Arc<Mutex<Option<String>>>) {
225 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
226 let port = listener.local_addr().unwrap().port();
227 let captured: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
228
229 let captured_in_cb = captured.clone();
230 let mut acceptor = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
231 acceptor.set_private_key(&key).unwrap();
232 acceptor.set_certificate(&cert).unwrap();
233 acceptor.set_servername_callback(move |ssl, _alert| {
234 *captured_in_cb.lock().unwrap() = ssl.servername(NameType::HOST_NAME).map(str::to_owned);
235 Ok(())
236 });
237 let acceptor = acceptor.build();
238
239 std::thread::spawn(move || {
240 if let Ok((stream, _)) = listener.accept() {
241 let _ = acceptor.accept(stream);
243 }
244 });
245
246 (port, captured)
247 }
248
249 fn config_for(port: u16, ca: &X509, tls_server_name: Option<&str>) -> Config {
250 let mut config = Config::new(format!("https://127.0.0.1:{port}").parse().unwrap());
251 config.root_cert = Some(vec![ca.to_der().unwrap()]);
252 config.tls_server_name = tls_server_name.map(str::to_owned);
253 config
254 }
255
256 fn connector_for(config: &Config) -> hyper_openssl::client::legacy::HttpsConnector<HttpConnector> {
257 let mut http = HttpConnector::new();
258 http.enforce_http(false);
259 config.openssl_https_connector_with_connector(http).unwrap()
260 }
261
262 #[tokio::test]
265 async fn tls_server_name_drives_sni_and_verification() {
266 let server_name = "kubernetes.example.com";
267 let (cert, key) = self_signed_cert(San::Dns(server_name));
268 let (port, captured_sni) = spawn_tls_server(cert.clone(), key);
269
270 let config = config_for(port, &cert, Some(server_name));
271 let uri: http::Uri = config.cluster_url.clone();
272
273 connector_for(&config)
274 .oneshot(uri)
275 .await
276 .expect("handshake should succeed when verification targets tls_server_name");
277
278 assert_eq!(
279 captured_sni.lock().unwrap().as_deref(),
280 Some(server_name),
281 "ClientHello SNI must equal tls_server_name, not the connection host"
282 );
283 }
284
285 #[tokio::test]
289 async fn without_tls_server_name_verification_uses_connection_host() {
290 let server_name = "kubernetes.example.com";
291 let (cert, key) = self_signed_cert(San::Dns(server_name));
292 let (port, _captured_sni) = spawn_tls_server(cert.clone(), key);
293
294 let config = config_for(port, &cert, None);
295 let uri: http::Uri = config.cluster_url.clone();
296
297 let result = connector_for(&config).oneshot(uri).await;
298 assert!(
299 result.is_err(),
300 "handshake must fail when the cert does not match the connection host"
301 );
302 }
303
304 #[tokio::test]
306 async fn tls_server_name_as_ip_verifies_without_sni() {
307 let (cert, key) = self_signed_cert(San::Ip("127.0.0.1"));
308 let (port, captured_sni) = spawn_tls_server(cert.clone(), key);
309
310 let config = config_for(port, &cert, Some("127.0.0.1"));
311 let uri: http::Uri = config.cluster_url.clone();
312
313 connector_for(&config)
314 .oneshot(uri)
315 .await
316 .expect("handshake should succeed when the IP tls_server_name matches the cert");
317
318 assert_eq!(
319 *captured_sni.lock().unwrap(),
320 None,
321 "SNI must not be sent for an IP tls_server_name"
322 );
323 }
324
325 #[tokio::test]
327 async fn accept_invalid_certs_skips_verification() {
328 let (cert, key) = self_signed_cert(San::Dns("kubernetes.example.com"));
329 let (port, _captured_sni) = spawn_tls_server(cert.clone(), key);
330
331 let mut config = config_for(port, &cert, None);
332 config.accept_invalid_certs = true;
333 let uri: http::Uri = config.cluster_url.clone();
334
335 connector_for(&config)
336 .oneshot(uri)
337 .await
338 .expect("handshake should succeed when accept_invalid_certs disables verification");
339 }
340}
341
342mod private {
343 pub trait Sealed {}
344 impl Sealed for super::Config {}
345}
346
347impl ConfigExt for Config {
348 fn base_uri_layer(&self) -> BaseUriLayer {
349 BaseUriLayer::new(self.cluster_url.clone())
350 }
351
352 fn auth_layer(&self) -> Result<Option<AuthLayer>> {
353 Ok(match Auth::try_from(&self.auth_info).map_err(Error::Auth)? {
354 Auth::None => None,
355 Auth::Basic(user, pass) => Some(AuthLayer(Either::Left(
356 AddAuthorizationLayer::basic(&user, pass.expose_secret()).as_sensitive(true),
357 ))),
358 Auth::Bearer(token) => Some(AuthLayer(Either::Left(
359 AddAuthorizationLayer::bearer(token.expose_secret()).as_sensitive(true),
360 ))),
361 Auth::RefreshableToken(refreshable) => {
362 Some(AuthLayer(Either::Right(AsyncFilterLayer::new(refreshable))))
363 }
364 Auth::Certificate(_client_certificate_data, _client_key_data, _) => None,
365 })
366 }
367
368 fn extra_headers_layer(&self) -> Result<ExtraHeadersLayer> {
369 let mut headers = self.headers.clone();
370 if let Some(impersonate_user) = &self.auth_info.impersonate {
371 headers.push((
372 HeaderName::from_static("impersonate-user"),
373 HeaderValue::from_str(impersonate_user)
374 .map_err(http::Error::from)
375 .map_err(Error::HttpError)?,
376 ));
377 }
378 if let Some(impersonate_uid) = &self.auth_info.impersonate_uid {
379 headers.push((
380 HeaderName::from_static("impersonate-uid"),
381 HeaderValue::from_str(impersonate_uid)
382 .map_err(http::Error::from)
383 .map_err(Error::HttpError)?,
384 ))
385 }
386 if let Some(impersonate_groups) = &self.auth_info.impersonate_groups {
387 for group in impersonate_groups {
388 headers.push((
389 HeaderName::from_static("impersonate-group"),
390 HeaderValue::from_str(group)
391 .map_err(http::Error::from)
392 .map_err(Error::HttpError)?,
393 ));
394 }
395 }
396 if let Some(impersonate_user_extra) = &self.auth_info.impersonate_user_extra {
397 for (extra_key, extra_values) in impersonate_user_extra {
398 let name = impersonate_extra_header_name(extra_key)?;
399 for value in extra_values {
400 headers.push((
401 name.clone(),
402 HeaderValue::from_str(value)
403 .map_err(http::Error::from)
404 .map_err(Error::HttpError)?,
405 ))
406 }
407 }
408 }
409 Ok(ExtraHeadersLayer {
410 headers: Arc::new(headers),
411 })
412 }
413
414 #[cfg(feature = "rustls-tls")]
415 fn rustls_client_config(&self) -> Result<rustls::ClientConfig> {
416 let identity = match self.exec_identity_pem().0 {
417 Some(identity) => Some(identity),
418 None => self.identity_pem()?,
419 };
420 let mut config = tls::rustls_tls::rustls_client_config(
421 identity.as_deref(),
422 self.root_cert.as_deref(),
423 self.accept_invalid_certs,
424 )
425 .map_err(Error::RustlsTls)?;
426
427 if !self.accept_invalid_certs
432 && let Some(path) = &self.root_cert_file
433 {
434 let verifier = tls::rustls_tls::ReloadingVerifier::new(path.clone()).map_err(Error::RustlsTls)?;
435 config.dangerous().set_certificate_verifier(Arc::new(verifier));
436 }
437 Ok(config)
438 }
439
440 #[cfg(feature = "rustls-tls")]
441 fn rustls_https_connector(&self) -> Result<hyper_rustls::HttpsConnector<HttpConnector>> {
442 let mut connector = HttpConnector::new();
443 connector.enforce_http(false);
444 self.rustls_https_connector_with_connector(connector)
445 }
446
447 #[cfg(feature = "rustls-tls")]
448 fn rustls_https_connector_with_connector<H>(
449 &self,
450 connector: H,
451 ) -> Result<hyper_rustls::HttpsConnector<H>> {
452 use hyper_rustls::FixedServerNameResolver;
453
454 use crate::client::tls::rustls_tls;
455
456 let rustls_config = self.rustls_client_config()?;
457 let mut builder = hyper_rustls::HttpsConnectorBuilder::new()
458 .with_tls_config(rustls_config)
459 .https_or_http();
460 if let Some(tsn) = self.tls_server_name.as_ref() {
461 builder = builder.with_server_name_resolver(FixedServerNameResolver::new(
462 tsn.clone()
463 .try_into()
464 .map_err(rustls_tls::Error::InvalidServerName)
465 .map_err(Error::RustlsTls)?,
466 ));
467 }
468 Ok(builder.enable_http1().wrap_connector(connector))
469 }
470
471 #[cfg(feature = "openssl-tls")]
472 fn openssl_ssl_connector_builder(&self) -> Result<openssl::ssl::SslConnectorBuilder> {
473 let identity = match self.exec_identity_pem().0 {
474 Some(identity) => Some(identity),
475 None => self.identity_pem()?,
476 };
477
478 tls::openssl_tls::ssl_connector_builder(identity.as_ref(), self.root_cert.as_ref())
481 .map_err(|e| Error::OpensslTls(tls::openssl_tls::Error::CreateSslConnector(e)))
482 }
483
484 #[cfg(feature = "openssl-tls")]
485 fn openssl_https_connector(
486 &self,
487 ) -> Result<hyper_openssl::client::legacy::HttpsConnector<HttpConnector>> {
488 let mut connector = HttpConnector::new();
489 connector.enforce_http(false);
490 self.openssl_https_connector_with_connector(connector)
491 }
492
493 #[cfg(feature = "openssl-tls")]
494 fn openssl_https_connector_with_connector<H>(
495 &self,
496 connector: H,
497 ) -> Result<hyper_openssl::client::legacy::HttpsConnector<H>>
498 where
499 H: tower::Service<http::Uri> + Send,
500 H::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
501 H::Future: Send + 'static,
502 H::Response: Read + Write + hyper_util::client::legacy::connect::Connection + Unpin,
503 {
504 let mut https = hyper_openssl::client::legacy::HttpsConnector::with_connector(
505 connector,
506 self.openssl_ssl_connector_builder()?,
507 )
508 .map_err(|e| Error::OpensslTls(tls::openssl_tls::Error::CreateHttpsConnector(e)))?;
509 let accept_invalid_certs = self.accept_invalid_certs;
512 let tls_server_name = self.tls_server_name.clone();
513 if accept_invalid_certs || tls_server_name.is_some() {
514 https.set_callback(move |ssl, _uri| {
515 if accept_invalid_certs {
516 ssl.set_verify(openssl::ssl::SslVerifyMode::NONE);
517 }
518 if let Some(name) = &tls_server_name {
519 use std::net::IpAddr;
520
521 use openssl::x509::verify::X509CheckFlags;
522 ssl.set_use_server_name_indication(false);
525 ssl.set_verify_hostname(false);
526 if name.parse::<IpAddr>().is_err() {
528 ssl.set_hostname(name)?;
529 }
530 let param = ssl.param_mut();
531 param.set_hostflags(X509CheckFlags::NO_PARTIAL_WILDCARDS);
532 match name.parse::<IpAddr>() {
533 Ok(ip) => param.set_ip(ip)?,
534 Err(_) => param.set_host(name)?,
535 }
536 }
537 Ok(())
538 });
539 }
540 Ok(https)
541 }
542}
543
544impl Config {
545 pub(crate) fn exec_identity_pem(&self) -> (Option<Vec<u8>>, Option<Timestamp>) {
550 match Auth::try_from(&self.auth_info) {
551 Ok(Auth::Certificate(client_certificate_data, client_key_data, expiration)) => {
552 const NEW_LINE: u8 = b'\n';
553
554 let mut buffer = client_key_data.expose_secret().as_bytes().to_vec();
555 buffer.push(NEW_LINE);
556 buffer.extend_from_slice(client_certificate_data.as_bytes());
557 buffer.push(NEW_LINE);
558 (Some(buffer), expiration)
559 }
560 _ => (None, None),
561 }
562 }
563}
564
565fn impersonate_extra_header_name(key: &str) -> Result<HeaderName> {
570 const NEEDS_ESCAPE: percent_encoding::AsciiSet = percent_encoding::NON_ALPHANUMERIC
573 .remove(b'!')
574 .remove(b'#')
575 .remove(b'$')
576 .remove(b'&')
577 .remove(b'\'')
578 .remove(b'*')
579 .remove(b'+')
580 .remove(b'-')
581 .remove(b'.')
582 .remove(b'^')
583 .remove(b'_')
584 .remove(b'`')
585 .remove(b'|')
586 .remove(b'~');
587
588 let encoded_key = percent_encoding::percent_encode(key.as_bytes(), &NEEDS_ESCAPE);
589 format!("impersonate-extra-{encoded_key}")
590 .try_into()
591 .map_err(http::Error::from)
592 .map_err(Error::HttpError)
593}
594
595#[cfg(test)]
596mod tests {
597 use super::impersonate_extra_header_name;
598
599 #[test]
600 fn impersonate_user_extra_no_unnecessary_escape() {
601 assert_eq!(
602 impersonate_extra_header_name("FOO-bar.baz_123").unwrap().as_str(),
603 "impersonate-extra-foo-bar.baz_123"
604 );
605 }
606
607 #[test]
608 fn impersonate_user_extra_escapes_percent() {
609 assert_eq!(
610 impersonate_extra_header_name("foo%2f").unwrap().as_str(),
611 "impersonate-extra-foo%252f"
612 );
613 }
614
615 #[test]
616 fn impersonate_user_extra_escapes_slash() {
617 assert_eq!(
618 impersonate_extra_header_name("foo.com/bar").unwrap().as_str(),
619 "impersonate-extra-foo.com%2fbar"
620 );
621 }
622
623 #[test]
624 fn impersonate_user_extra_roundtrips() {
625 for chr in 0..128 {
626 let key = str::from_utf8(&[chr]).unwrap().to_lowercase();
627 let encoded = impersonate_extra_header_name(&key).unwrap();
628 let decoded = percent_encoding::percent_decode_str(encoded.as_str())
629 .decode_utf8()
630 .unwrap();
631
632 assert_eq!(format!("impersonate-extra-{key}"), decoded);
633 }
634 }
635}