Skip to main content

kube_client/client/
config_ext.rs

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
17/// Extensions to [`Config`](crate::Config) for custom [`Client`](crate::Client).
18///
19/// See [`Client::new`](crate::Client::new) for an example.
20///
21/// This trait is sealed and cannot be implemented.
22pub trait ConfigExt: private::Sealed {
23    /// Layer to set the base URI of requests to the configured server.
24    fn base_uri_layer(&self) -> BaseUriLayer;
25
26    /// Optional layer to set up `Authorization` header depending on the config.
27    fn auth_layer(&self) -> Result<Option<AuthLayer>>;
28
29    /// Layer to add non-authn HTTP headers depending on the config.
30    fn extra_headers_layer(&self) -> Result<ExtraHeadersLayer>;
31
32    /// Create [`hyper_rustls::HttpsConnector`] based on config.
33    ///
34    /// # Example
35    ///
36    /// ```rust
37    /// # async fn doc() -> Result<(), Box<dyn std::error::Error>> {
38    /// # use kube::{client::{Body, ConfigExt}, Config};
39    /// # use hyper_util::rt::TokioExecutor;
40    /// let config = Config::infer().await?;
41    /// let https = config.rustls_https_connector()?;
42    /// let hyper_client: hyper_util::client::legacy::Client<_, Body> = hyper_util::client::legacy::Client::builder(TokioExecutor::new()).build(https);
43    /// # Ok(())
44    /// # }
45    /// ```
46    #[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    /// Create [`hyper_rustls::HttpsConnector`] based on config and `connector`.
51    ///
52    /// # Example
53    ///
54    /// ```rust
55    /// # async fn doc() -> Result<(), Box<dyn std::error::Error>> {
56    /// # use kube::{client::{Body, ConfigExt}, Config};
57    /// # use hyper_util::{client::legacy::connect::HttpConnector, rt::TokioExecutor};
58    /// let config = Config::infer().await?;
59    /// let mut connector = HttpConnector::new();
60    /// connector.enforce_http(false);
61    /// let https = config.rustls_https_connector_with_connector(connector)?;
62    /// let hyper_client: hyper_util::client::legacy::Client<_, Body> = hyper_util::client::legacy::Client::builder(TokioExecutor::new()).build(https);
63    /// # Ok(())
64    /// # }
65    /// ```
66    #[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    /// Create [`rustls::ClientConfig`] based on config.
74    /// # Example
75    ///
76    /// ```rust
77    /// # async fn doc() -> Result<(), Box<dyn std::error::Error>> {
78    /// # use hyper_util::client::legacy::connect::HttpConnector;
79    /// # use kube::{client::ConfigExt, Config};
80    /// let config = Config::infer().await?;
81    /// let https = {
82    ///     let rustls_config = std::sync::Arc::new(config.rustls_client_config()?);
83    ///     let mut http = HttpConnector::new();
84    ///     http.enforce_http(false);
85    ///     hyper_rustls::HttpsConnector::from((http, rustls_config))
86    /// };
87    /// # Ok(())
88    /// # }
89    /// ```
90    #[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
91    #[cfg(feature = "rustls-tls")]
92    fn rustls_client_config(&self) -> Result<rustls::ClientConfig>;
93
94    /// Create [`hyper_openssl::HttpsConnector`] based on config.
95    /// # Example
96    ///
97    /// ```rust
98    /// # async fn doc() -> Result<(), Box<dyn std::error::Error>> {
99    /// # use kube::{client::ConfigExt, Config};
100    /// let config = Config::infer().await?;
101    /// let https = config.openssl_https_connector()?;
102    /// # Ok(())
103    /// # }
104    /// ```
105    #[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    /// Create [`hyper_openssl::HttpsConnector`] based on config and `connector`.
111    /// # Example
112    ///
113    /// ```rust
114    /// # async fn doc() -> Result<(), Box<dyn std::error::Error>> {
115    /// # use hyper_util::client::legacy::connect::HttpConnector;
116    /// # use kube::{client::ConfigExt, Config};
117    /// let mut http = HttpConnector::new();
118    /// http.enforce_http(false);
119    /// let config = Config::infer().await?;
120    /// let https = config.openssl_https_connector_with_connector(http)?;
121    /// # Ok(())
122    /// # }
123    /// ```
124    #[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    /// Create [`openssl::ssl::SslConnectorBuilder`] based on config.
137    /// # Example
138    ///
139    /// ```rust
140    /// # async fn doc() -> Result<(), Box<dyn std::error::Error>> {
141    /// # use hyper_util::client::legacy::connect::HttpConnector;
142    /// # use kube::{client::ConfigExt, Client, Config};
143    /// let config = Config::infer().await?;
144    /// let https = {
145    ///     let mut http = HttpConnector::new();
146    ///     http.enforce_http(false);
147    ///     let ssl = config.openssl_ssl_connector_builder()?;
148    ///     hyper_openssl::client::legacy::HttpsConnector::with_connector(http, ssl)?
149    /// };
150    /// # Ok(())
151    /// # }
152    /// ```
153    #[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            extension::{BasicConstraints, SubjectAlternativeName},
173            X509NameBuilder, X509,
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    // Self-signed cert with a single SAN. A DNS SAN won't match the 127.0.0.1 we connect to, so
186    // verification only passes if the verify-host comes from tls_server_name.
187    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.set_not_before(&Asn1Time::days_from_now(0).unwrap()).unwrap();
203        builder.set_not_after(&Asn1Time::days_from_now(1).unwrap()).unwrap();
204        builder
205            .append_extension(BasicConstraints::new().critical().ca().build().unwrap())
206            .unwrap();
207        let mut san_ext = SubjectAlternativeName::new();
208        match san {
209            San::Dns(s) => san_ext.dns(s),
210            San::Ip(s) => san_ext.ip(s),
211        };
212        let san_ext = san_ext.build(&builder.x509v3_context(None, None)).unwrap();
213        builder.append_extension(san_ext).unwrap();
214        builder.sign(&pkey, MessageDigest::sha256()).unwrap();
215
216        (builder.build(), pkey)
217    }
218
219    // Localhost TLS server that records the SNI from the one connection it accepts.
220    fn spawn_tls_server(cert: X509, key: PKey<Private>) -> (u16, Arc<Mutex<Option<String>>>) {
221        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
222        let port = listener.local_addr().unwrap().port();
223        let captured: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
224
225        let captured_in_cb = captured.clone();
226        let mut acceptor = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
227        acceptor.set_private_key(&key).unwrap();
228        acceptor.set_certificate(&cert).unwrap();
229        acceptor.set_servername_callback(move |ssl, _alert| {
230            *captured_in_cb.lock().unwrap() = ssl.servername(NameType::HOST_NAME).map(str::to_owned);
231            Ok(())
232        });
233        let acceptor = acceptor.build();
234
235        std::thread::spawn(move || {
236            if let Ok((stream, _)) = listener.accept() {
237                // SNI is captured during the handshake; we don't care whether it then completes.
238                let _ = acceptor.accept(stream);
239            }
240        });
241
242        (port, captured)
243    }
244
245    fn config_for(port: u16, ca: &X509, tls_server_name: Option<&str>) -> Config {
246        let mut config = Config::new(format!("https://127.0.0.1:{port}").parse().unwrap());
247        config.root_cert = Some(vec![ca.to_der().unwrap()]);
248        config.tls_server_name = tls_server_name.map(str::to_owned);
249        config
250    }
251
252    fn connector_for(config: &Config) -> hyper_openssl::client::legacy::HttpsConnector<HttpConnector> {
253        let mut http = HttpConnector::new();
254        http.enforce_http(false);
255        config.openssl_https_connector_with_connector(http).unwrap()
256    }
257
258    // tls_server_name set: SNI carries it (not the 127.0.0.1 host) and verification targets it,
259    // so the handshake against the SAN-only cert succeeds.
260    #[tokio::test]
261    async fn tls_server_name_drives_sni_and_verification() {
262        let server_name = "kubernetes.example.com";
263        let (cert, key) = self_signed_cert(San::Dns(server_name));
264        let (port, captured_sni) = spawn_tls_server(cert.clone(), key);
265
266        let config = config_for(port, &cert, Some(server_name));
267        let uri: http::Uri = config.cluster_url.clone();
268
269        connector_for(&config)
270            .oneshot(uri)
271            .await
272            .expect("handshake should succeed when verification targets tls_server_name");
273
274        assert_eq!(
275            captured_sni.lock().unwrap().as_deref(),
276            Some(server_name),
277            "ClientHello SNI must equal tls_server_name, not the connection host"
278        );
279    }
280
281    // Control: without tls_server_name, verification falls back to the 127.0.0.1 host, which the
282    // SAN-only cert doesn't match, so the handshake fails. Confirms the pass above isn't just lax
283    // verification.
284    #[tokio::test]
285    async fn without_tls_server_name_verification_uses_connection_host() {
286        let server_name = "kubernetes.example.com";
287        let (cert, key) = self_signed_cert(San::Dns(server_name));
288        let (port, _captured_sni) = spawn_tls_server(cert.clone(), key);
289
290        let config = config_for(port, &cert, None);
291        let uri: http::Uri = config.cluster_url.clone();
292
293        let result = connector_for(&config).oneshot(uri).await;
294        assert!(
295            result.is_err(),
296            "handshake must fail when the cert does not match the connection host"
297        );
298    }
299
300    // An IP tls_server_name verifies via set_ip and, per RFC 6066, sends no SNI.
301    #[tokio::test]
302    async fn tls_server_name_as_ip_verifies_without_sni() {
303        let (cert, key) = self_signed_cert(San::Ip("127.0.0.1"));
304        let (port, captured_sni) = spawn_tls_server(cert.clone(), key);
305
306        let config = config_for(port, &cert, Some("127.0.0.1"));
307        let uri: http::Uri = config.cluster_url.clone();
308
309        connector_for(&config)
310            .oneshot(uri)
311            .await
312            .expect("handshake should succeed when the IP tls_server_name matches the cert");
313
314        assert_eq!(
315            *captured_sni.lock().unwrap(),
316            None,
317            "SNI must not be sent for an IP tls_server_name"
318        );
319    }
320
321    // accept_invalid_certs disables verification, so the mismatched cert is accepted.
322    #[tokio::test]
323    async fn accept_invalid_certs_skips_verification() {
324        let (cert, key) = self_signed_cert(San::Dns("kubernetes.example.com"));
325        let (port, _captured_sni) = spawn_tls_server(cert.clone(), key);
326
327        let mut config = config_for(port, &cert, None);
328        config.accept_invalid_certs = true;
329        let uri: http::Uri = config.cluster_url.clone();
330
331        connector_for(&config)
332            .oneshot(uri)
333            .await
334            .expect("handshake should succeed when accept_invalid_certs disables verification");
335    }
336}
337
338mod private {
339    pub trait Sealed {}
340    impl Sealed for super::Config {}
341}
342
343impl ConfigExt for Config {
344    fn base_uri_layer(&self) -> BaseUriLayer {
345        BaseUriLayer::new(self.cluster_url.clone())
346    }
347
348    fn auth_layer(&self) -> Result<Option<AuthLayer>> {
349        Ok(match Auth::try_from(&self.auth_info).map_err(Error::Auth)? {
350            Auth::None => None,
351            Auth::Basic(user, pass) => Some(AuthLayer(Either::Left(
352                AddAuthorizationLayer::basic(&user, pass.expose_secret()).as_sensitive(true),
353            ))),
354            Auth::Bearer(token) => Some(AuthLayer(Either::Left(
355                AddAuthorizationLayer::bearer(token.expose_secret()).as_sensitive(true),
356            ))),
357            Auth::RefreshableToken(refreshable) => {
358                Some(AuthLayer(Either::Right(AsyncFilterLayer::new(refreshable))))
359            }
360            Auth::Certificate(_client_certificate_data, _client_key_data, _) => None,
361        })
362    }
363
364    fn extra_headers_layer(&self) -> Result<ExtraHeadersLayer> {
365        let mut headers = self.headers.clone();
366        if let Some(impersonate_user) = &self.auth_info.impersonate {
367            headers.push((
368                HeaderName::from_static("impersonate-user"),
369                HeaderValue::from_str(impersonate_user)
370                    .map_err(http::Error::from)
371                    .map_err(Error::HttpError)?,
372            ));
373        }
374        if let Some(impersonate_groups) = &self.auth_info.impersonate_groups {
375            for group in impersonate_groups {
376                headers.push((
377                    HeaderName::from_static("impersonate-group"),
378                    HeaderValue::from_str(group)
379                        .map_err(http::Error::from)
380                        .map_err(Error::HttpError)?,
381                ));
382            }
383        }
384        Ok(ExtraHeadersLayer {
385            headers: Arc::new(headers),
386        })
387    }
388
389    #[cfg(feature = "rustls-tls")]
390    fn rustls_client_config(&self) -> Result<rustls::ClientConfig> {
391        let identity = match self.exec_identity_pem().0 {
392            Some(identity) => Some(identity),
393            None => self.identity_pem()?,
394        };
395        let mut config = tls::rustls_tls::rustls_client_config(
396            identity.as_deref(),
397            self.root_cert.as_deref(),
398            self.accept_invalid_certs,
399        )
400        .map_err(Error::RustlsTls)?;
401
402        // When a CA file path is available (in-cluster), install a verifier
403        // that re-reads it periodically to survive CA rotation. `root_cert`
404        // bytes are still passed above so the builder typestate is satisfied,
405        // but verification is handed over here.
406        if !self.accept_invalid_certs
407            && let Some(path) = &self.root_cert_file
408        {
409            let verifier =
410                tls::rustls_tls::ReloadingVerifier::new(path.clone()).map_err(Error::RustlsTls)?;
411            config
412                .dangerous()
413                .set_certificate_verifier(Arc::new(verifier));
414        }
415        Ok(config)
416    }
417
418    #[cfg(feature = "rustls-tls")]
419    fn rustls_https_connector(&self) -> Result<hyper_rustls::HttpsConnector<HttpConnector>> {
420        let mut connector = HttpConnector::new();
421        connector.enforce_http(false);
422        self.rustls_https_connector_with_connector(connector)
423    }
424
425    #[cfg(feature = "rustls-tls")]
426    fn rustls_https_connector_with_connector<H>(
427        &self,
428        connector: H,
429    ) -> Result<hyper_rustls::HttpsConnector<H>> {
430        use hyper_rustls::FixedServerNameResolver;
431
432        use crate::client::tls::rustls_tls;
433
434        let rustls_config = self.rustls_client_config()?;
435        let mut builder = hyper_rustls::HttpsConnectorBuilder::new()
436            .with_tls_config(rustls_config)
437            .https_or_http();
438        if let Some(tsn) = self.tls_server_name.as_ref() {
439            builder = builder.with_server_name_resolver(FixedServerNameResolver::new(
440                tsn.clone()
441                    .try_into()
442                    .map_err(rustls_tls::Error::InvalidServerName)
443                    .map_err(Error::RustlsTls)?,
444            ));
445        }
446        Ok(builder.enable_http1().wrap_connector(connector))
447    }
448
449    #[cfg(feature = "openssl-tls")]
450    fn openssl_ssl_connector_builder(&self) -> Result<openssl::ssl::SslConnectorBuilder> {
451        let identity = match self.exec_identity_pem().0 {
452            Some(identity) => Some(identity),
453            None => self.identity_pem()?,
454        };
455
456        // tls_server_name has no hook on the builder; it is applied per-connection in
457        // openssl_https_connector_with_connector instead.
458        tls::openssl_tls::ssl_connector_builder(identity.as_ref(), self.root_cert.as_ref())
459            .map_err(|e| Error::OpensslTls(tls::openssl_tls::Error::CreateSslConnector(e)))
460    }
461
462    #[cfg(feature = "openssl-tls")]
463    fn openssl_https_connector(
464        &self,
465    ) -> Result<hyper_openssl::client::legacy::HttpsConnector<HttpConnector>> {
466        let mut connector = HttpConnector::new();
467        connector.enforce_http(false);
468        self.openssl_https_connector_with_connector(connector)
469    }
470
471    #[cfg(feature = "openssl-tls")]
472    fn openssl_https_connector_with_connector<H>(
473        &self,
474        connector: H,
475    ) -> Result<hyper_openssl::client::legacy::HttpsConnector<H>>
476    where
477        H: tower::Service<http::Uri> + Send,
478        H::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
479        H::Future: Send + 'static,
480        H::Response: Read + Write + hyper_util::client::legacy::connect::Connection + Unpin,
481    {
482        let mut https = hyper_openssl::client::legacy::HttpsConnector::with_connector(
483            connector,
484            self.openssl_ssl_connector_builder()?,
485        )
486        .map_err(|e| Error::OpensslTls(tls::openssl_tls::Error::CreateHttpsConnector(e)))?;
487        // OpenSSL has no server-name-resolver hook, so unlike rustls we apply tls_server_name in
488        // the per-connection callback (which already exists for accept_invalid_certs).
489        let accept_invalid_certs = self.accept_invalid_certs;
490        let tls_server_name = self.tls_server_name.clone();
491        if accept_invalid_certs || tls_server_name.is_some() {
492            https.set_callback(move |ssl, _uri| {
493                if accept_invalid_certs {
494                    ssl.set_verify(openssl::ssl::SslVerifyMode::NONE);
495                }
496                if let Some(name) = &tls_server_name {
497                    use std::net::IpAddr;
498
499                    use openssl::x509::verify::X509CheckFlags;
500                    // into_ssl(host) runs after this callback and would otherwise set SNI and the
501                    // verify host from the URI host. Disable both so it keeps our values.
502                    ssl.set_use_server_name_indication(false);
503                    ssl.set_verify_hostname(false);
504                    // SNI is not sent for IP literals.
505                    if name.parse::<IpAddr>().is_err() {
506                        ssl.set_hostname(name)?;
507                    }
508                    let param = ssl.param_mut();
509                    param.set_hostflags(X509CheckFlags::NO_PARTIAL_WILDCARDS);
510                    match name.parse::<IpAddr>() {
511                        Ok(ip) => param.set_ip(ip)?,
512                        Err(_) => param.set_host(name)?,
513                    }
514                }
515                Ok(())
516            });
517        }
518        Ok(https)
519    }
520}
521
522impl Config {
523    // This is necessary to retrieve an identity when an exec plugin
524    // returns a client certificate and key instead of a token.
525    // This has be to be checked on TLS configuration vs tokens
526    // which can be added in as an AuthLayer.
527    pub(crate) fn exec_identity_pem(&self) -> (Option<Vec<u8>>, Option<Timestamp>) {
528        match Auth::try_from(&self.auth_info) {
529            Ok(Auth::Certificate(client_certificate_data, client_key_data, expiration)) => {
530                const NEW_LINE: u8 = b'\n';
531
532                let mut buffer = client_key_data.expose_secret().as_bytes().to_vec();
533                buffer.push(NEW_LINE);
534                buffer.extend_from_slice(client_certificate_data.as_bytes());
535                buffer.push(NEW_LINE);
536                (Some(buffer), expiration)
537            }
538            _ => (None, None),
539        }
540    }
541}