diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd3844af3..ebdff21a9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,6 +47,9 @@ jobs: - name: Test kube with features rustls-tls,ws,oauth run: cargo test -p kube --lib --no-default-features --features=rustls-tls,ws,oauth if: matrix.os == 'ubuntu-latest' + - name: Test kube with features openssl-tls,ws,oauth + run: cargo test -p kube --lib --no-default-features --features=openssl-tls,ws,oauth + if: matrix.os == 'ubuntu-latest' # Feature tests in examples - name: Test crd_derive_no_schema example run: cargo test -p examples --example crd_derive_no_schema --no-default-features --features=native-tls,latest diff --git a/CHANGELOG.md b/CHANGELOG.md index d057a0950..40f9b13de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ UNRELEASED - If you currently disable default `kube-derive` default features to avoid automatic schema generation, add `#[kube(schema = "disabled")]` to your spec struct instead * BREAKING: Moved `CustomResource` derive crate overrides into subattribute `#[kube(crates(...))]` - #690 - Replace `#[kube(kube_core = .., k8s_openapi = .., schema = .., serde = .., serde_json = ..)]` with `#[kube(crates(kube_core = .., k8s_openapi = .., schema = .., serde = .., serde_json = ..))]` + * Added `openssl-tls` feature to use `openssl` for TLS on all platforms. Note that, even though `native-tls` uses a platform specific TLS, `kube` requires `openssl` on all platforms because `native-tls` only allows PKCS12 input to load certificates and private key at the moment, and creating PKCS12 requires `openssl`. - #700 + ### Refining Errors We started working on improving error ergonomics (tracking issue: #688). diff --git a/kube-client/Cargo.toml b/kube-client/Cargo.toml index ed6beeb03..8d579e746 100644 --- a/kube-client/Cargo.toml +++ b/kube-client/Cargo.toml @@ -19,6 +19,7 @@ edition = "2021" default = ["client", "native-tls"] native-tls = ["openssl", "hyper-tls", "tokio-native-tls"] rustls-tls = ["rustls", "rustls-pemfile", "hyper-rustls", "webpki"] +openssl-tls = ["openssl", "hyper-openssl"] ws = ["client", "tokio-tungstenite", "rand", "kube-core/ws"] oauth = ["client", "tame-oauth"] gzip = ["client", "tower-http/decompression-gzip"] @@ -32,7 +33,7 @@ deprecated-crd-v1beta1 = ["kube-core/deprecated-crd-v1beta1"] __non_core = ["tracing", "serde_yaml", "base64"] [package.metadata.docs.rs] -features = ["client", "native-tls", "rustls-tls", "ws", "oauth", "jsonpatch", "admission", "k8s-openapi/v1_22"] +features = ["client", "native-tls", "rustls-tls", "openssl-tls", "ws", "oauth", "jsonpatch", "admission", "k8s-openapi/v1_22"] # Define the configuration attribute `docsrs`. Used to enable `doc_cfg` feature. rustdoc-args = ["--cfg", "docsrs"] @@ -70,6 +71,7 @@ tame-oauth = { version = "0.4.7", features = ["gcp"], optional = true } pin-project = { version = "1.0.4", optional = true } rand = { version = "0.8.3", optional = true } tracing = { version = "0.1.29", features = ["log"], optional = true } +hyper-openssl = { version = "0.9.1", optional = true } [dependencies.k8s-openapi] version = "0.13.1" diff --git a/kube-client/src/client/auth/oauth.rs b/kube-client/src/client/auth/oauth.rs index 90750002b..6f30249b2 100644 --- a/kube-client/src/client/auth/oauth.rs +++ b/kube-client/src/client/auth/oauth.rs @@ -52,6 +52,12 @@ pub enum Error { /// OAuth failed with unknown reason #[error("unknown OAuth error: {0}")] Unknown(String), + + /// Failed to create OpenSSL HTTPS connector + #[cfg(feature = "openssl-tls")] + #[cfg_attr(docsrs, doc(cfg(feature = "openssl-tls")))] + #[error("failed to create OpenSSL HTTPS connector: {0}")] + CreateOpensslHttpsConnector(#[source] openssl::error::ErrorStack), } pub(crate) struct Gcp { @@ -94,16 +100,25 @@ impl Gcp { Ok(TokenOrRequest::Request { request, scope_hash, .. }) => { - #[cfg(not(any(feature = "native-tls", feature = "rustls-tls")))] + #[cfg(not(any(feature = "native-tls", feature = "rustls-tls", feature = "openssl-tls")))] compile_error!( - "At least one of native-tls or rustls-tls feature must be enabled to use oauth feature" + "At least one of native-tls or rustls-tls or openssl-tls feature must be enabled to use oauth feature" ); - // If both are enabled, we use rustls unlike `Client` because there's no need to support ip v4/6 subject matching. - // TODO Allow users to choose when both are enabled. - #[cfg(feature = "rustls-tls")] - let https = hyper_rustls::HttpsConnector::with_native_roots(); - #[cfg(all(not(feature = "rustls-tls"), feature = "native-tls"))] + // Current TLS feature precedence when more than one are set: + // 1. openssl-tls + // 2. native-tls + // 3. rustls-tls + #[cfg(feature = "openssl-tls")] + let https = + hyper_openssl::HttpsConnector::new().map_err(Error::CreateOpensslHttpsConnector)?; + #[cfg(all(not(feature = "openssl-tls"), feature = "native-tls"))] let https = hyper_tls::HttpsConnector::new(); + #[cfg(all( + not(any(feature = "openssl-tls", feature = "native-tls")), + feature = "rustls-tls" + ))] + let https = hyper_rustls::HttpsConnector::with_native_roots(); + let client = hyper::Client::builder().build::<_, hyper::Body>(https); let res = client diff --git a/kube-client/src/client/config_ext.rs b/kube-client/src/client/config_ext.rs index d95838d40..6868b798c 100644 --- a/kube-client/src/client/config_ext.rs +++ b/kube-client/src/client/config_ext.rs @@ -1,6 +1,7 @@ use tower::util::Either; -#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] use super::tls; +#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "openssl-tls"))] +use super::tls; use super::{ auth::Auth, middleware::{AddAuthorizationLayer, AuthLayer, BaseUriLayer, RefreshTokenLayer}, @@ -96,6 +97,63 @@ pub trait ConfigExt: private::Sealed { #[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))] #[cfg(feature = "rustls-tls")] fn rustls_client_config(&self) -> Result; + + /// Create [`hyper_openssl::HttpsConnector`] based on config. + /// # Example + /// + /// ```rust + /// # async fn doc() -> Result<(), Box> { + /// # use kube::{client::ConfigExt, Config}; + /// let config = Config::infer().await?; + /// let https = config.openssl_https_connector()?; + /// # Ok(()) + /// # } + /// ``` + #[cfg_attr(docsrs, doc(cfg(feature = "openssl-tls")))] + #[cfg(feature = "openssl-tls")] + fn openssl_https_connector(&self) -> Result>; + + /// Create [`hyper_openssl::HttpsConnector`] based on config and `connector`. + /// # Example + /// + /// ```rust + /// # async fn doc() -> Result<(), Box> { + /// # use hyper::client::HttpConnector; + /// # use kube::{client::ConfigExt, Config}; + /// let mut http = HttpConnector::new(); + /// http.enforce_http(false); + /// let config = Config::infer().await?; + /// let https = config.openssl_https_connector_with_connector(http)?; + /// # Ok(()) + /// # } + /// ``` + #[cfg_attr(docsrs, doc(cfg(feature = "openssl-tls")))] + #[cfg(feature = "openssl-tls")] + fn openssl_https_connector_with_connector( + &self, + connector: hyper::client::HttpConnector, + ) -> Result>; + + /// Create [`openssl::ssl::SslConnectorBuilder`] based on config. + /// # Example + /// + /// ```rust + /// # async fn doc() -> Result<(), Box> { + /// # use hyper::client::HttpConnector; + /// # use kube::{client::ConfigExt, Client, Config}; + /// let config = Config::infer().await?; + /// let https = { + /// let mut http = HttpConnector::new(); + /// http.enforce_http(false); + /// let ssl = config.openssl_ssl_connector_builder()?; + /// hyper_openssl::HttpsConnector::with_connector(http, ssl)? + /// }; + /// # Ok(()) + /// # } + /// ``` + #[cfg_attr(docsrs, doc(cfg(feature = "openssl-tls")))] + #[cfg(feature = "openssl-tls")] + fn openssl_ssl_connector_builder(&self) -> Result; } mod private { @@ -154,4 +212,34 @@ impl ConfigExt for Config { http.enforce_http(false); Ok(hyper_rustls::HttpsConnector::from((http, rustls_config))) } + + #[cfg(feature = "openssl-tls")] + fn openssl_ssl_connector_builder(&self) -> Result { + tls::openssl_tls::ssl_connector_builder(self.identity_pem.as_ref(), self.root_cert.as_ref()) + .map_err(|e| Error::OpensslTls(tls::openssl_tls::Error::CreateSslConnector(e))) + } + + #[cfg(feature = "openssl-tls")] + fn openssl_https_connector(&self) -> Result> { + let mut connector = hyper::client::HttpConnector::new(); + connector.enforce_http(false); + self.openssl_https_connector_with_connector(connector) + } + + #[cfg(feature = "openssl-tls")] + fn openssl_https_connector_with_connector( + &self, + connector: hyper::client::HttpConnector, + ) -> Result> { + let mut https = + hyper_openssl::HttpsConnector::with_connector(connector, self.openssl_ssl_connector_builder()?) + .map_err(|e| Error::OpensslTls(tls::openssl_tls::Error::CreateHttpsConnector(e)))?; + if self.accept_invalid_certs { + https.set_callback(|ssl, _uri| { + ssl.set_verify(openssl::ssl::SslVerifyMode::NONE); + Ok(()) + }); + } + Ok(https) + } } diff --git a/kube-client/src/client/mod.rs b/kube-client/src/client/mod.rs index 9b13d9dba..47c6c0209 100644 --- a/kube-client/src/client/mod.rs +++ b/kube-client/src/client/mod.rs @@ -38,7 +38,10 @@ mod config_ext; pub use auth::Error as AuthError; pub use config_ext::ConfigExt; pub mod middleware; -#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] mod tls; +#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "openssl-tls"))] +mod tls; +#[cfg(feature = "openssl-tls")] +pub use tls::openssl_tls::Error as OpensslTlsError; #[cfg(feature = "ws")] mod upgrade; #[cfg(feature = "oauth")] @@ -460,15 +463,23 @@ impl TryFrom for Client { let mut connector = HttpConnector::new(); connector.enforce_http(false); - // Note that if both `native_tls` and `rustls` is enabled, `native_tls` is used by default. - // To use `rustls`, disable `native_tls` or create custom client. - // If tls features are not enabled, http connector will be used. - #[cfg(feature = "native-tls")] + // Current TLS feature precedence when more than one are set: + // 1. openssl-tls + // 2. native-tls + // 3. rustls-tls + // Create a custom client to use something else. + // If TLS features are not enabled, http connector will be used. + #[cfg(feature = "openssl-tls")] + let connector = config.openssl_https_connector_with_connector(connector)?; + #[cfg(all(not(feature = "openssl-tls"), feature = "native-tls"))] let connector = hyper_tls::HttpsConnector::from(( connector, tokio_native_tls::TlsConnector::from(config.native_tls_connector()?), )); - #[cfg(all(not(feature = "native-tls"), feature = "rustls-tls"))] + #[cfg(all( + not(any(feature = "openssl-tls", feature = "native-tls")), + feature = "rustls-tls" + ))] let connector = hyper_rustls::HttpsConnector::from(( connector, std::sync::Arc::new(config.rustls_client_config()?), diff --git a/kube-client/src/client/tls.rs b/kube-client/src/client/tls.rs index 06cd5d482..005344cf6 100644 --- a/kube-client/src/client/tls.rs +++ b/kube-client/src/client/tls.rs @@ -160,3 +160,105 @@ pub mod rustls_tls { } } } + +#[cfg(feature = "openssl-tls")] +pub mod openssl_tls { + use openssl::{ + pkey::PKey, + ssl::{SslConnector, SslConnectorBuilder, SslMethod}, + x509::X509, + }; + use thiserror::Error; + + /// Errors from OpenSSL TLS + #[derive(Debug, Error)] + pub enum Error { + /// Failed to create OpenSSL HTTPS connector + #[error("failed to create OpenSSL HTTPS connector: {0}")] + CreateHttpsConnector(#[source] openssl::error::ErrorStack), + + /// Failed to create OpenSSL SSL connector + #[error("failed to create OpenSSL SSL connector: {0}")] + CreateSslConnector(#[source] SslConnectorError), + } + + /// Errors from creating a `SslConnectorBuilder` + #[derive(Debug, Error)] + pub enum SslConnectorError { + /// Failed to build SslConnectorBuilder + #[error("failed to build SslConnectorBuilder: {0}")] + CreateBuilder(#[source] openssl::error::ErrorStack), + + /// Failed to deserialize PEM-encoded chain of certificates + #[error("failed to deserialize PEM-encoded chain of certificates: {0}")] + DeserializeCertificateChain(#[source] openssl::error::ErrorStack), + + /// Failed to deserialize PEM-encoded private key + #[error("failed to deserialize PEM-encoded private key: {0}")] + DeserializePrivateKey(#[source] openssl::error::ErrorStack), + + /// Failed to set private key + #[error("failed to set private key: {0}")] + SetPrivateKey(#[source] openssl::error::ErrorStack), + + /// Failed to get a leaf certificate, the certificate chain is empty + #[error("failed to get a leaf certificate, the certificate chain is empty")] + GetLeafCertificate, + + /// Failed to set the leaf certificate + #[error("failed to set the leaf certificate: {0}")] + SetLeafCertificate(#[source] openssl::error::ErrorStack), + + /// Failed to append a certificate to the chain + #[error("failed to append a certificate to the chain: {0}")] + AppendCertificate(#[source] openssl::error::ErrorStack), + + /// Failed to deserialize DER-encoded root certificate + #[error("failed to deserialize DER-encoded root certificate: {0}")] + DeserializeRootCertificate(#[source] openssl::error::ErrorStack), + + /// Failed to add a root certificate + #[error("failed to add a root certificate: {0}")] + AddRootCertificate(#[source] openssl::error::ErrorStack), + } + + /// Create `openssl::ssl::SslConnectorBuilder` required for `hyper_openssl::HttpsConnector`. + pub fn ssl_connector_builder( + identity_pem: Option<&Vec>, + root_certs: Option<&Vec>>, + ) -> Result { + let mut builder = + SslConnector::builder(SslMethod::tls()).map_err(SslConnectorError::CreateBuilder)?; + if let Some(pem) = identity_pem { + let mut chain = X509::stack_from_pem(pem) + .map_err(SslConnectorError::DeserializeCertificateChain)? + .into_iter(); + let leaf_cert = chain.next().ok_or(SslConnectorError::GetLeafCertificate)?; + builder + .set_certificate(&leaf_cert) + .map_err(SslConnectorError::SetLeafCertificate)?; + for cert in chain { + builder + .add_extra_chain_cert(cert) + .map_err(SslConnectorError::AppendCertificate)?; + } + + let pkey = PKey::private_key_from_pem(pem).map_err(SslConnectorError::DeserializePrivateKey)?; + builder + .set_private_key(&pkey) + .map_err(SslConnectorError::SetPrivateKey)?; + } + + if let Some(ders) = root_certs { + for der in ders { + let cert = X509::from_der(der).map_err(SslConnectorError::DeserializeRootCertificate)?; + builder + .cert_store_mut() + .add_cert(cert) + .map_err(SslConnectorError::AddRootCertificate)?; + } + } + + Ok(builder) + } +} diff --git a/kube-client/src/error.rs b/kube-client/src/error.rs index 670d5109b..fc3ef0b3a 100644 --- a/kube-client/src/error.rs +++ b/kube-client/src/error.rs @@ -69,6 +69,12 @@ pub enum Error { #[error("OpensslError: {0}")] OpensslError(#[source] openssl::error::ErrorStack), + /// Errors from OpenSSL TLS + #[cfg(feature = "openssl-tls")] + #[cfg_attr(docsrs, doc(feature = "openssl-tls"))] + #[error("openssl tls error: {0}")] + OpensslTls(#[source] crate::client::OpensslTlsError), + /// Failed to upgrade to a WebSocket connection #[cfg(feature = "ws")] #[cfg_attr(docsrs, doc(cfg(feature = "ws")))] diff --git a/kube/Cargo.toml b/kube/Cargo.toml index 0b425a6d8..1b6009732 100644 --- a/kube/Cargo.toml +++ b/kube/Cargo.toml @@ -19,6 +19,7 @@ edition = "2021" default = ["client", "native-tls"] native-tls = ["kube-client/native-tls"] rustls-tls = ["kube-client/rustls-tls"] +openssl-tls = ["kube-client/openssl-tls"] ws = ["kube-client/ws", "kube-core/ws"] oauth = ["kube-client/oauth"] gzip = ["kube-client/gzip"] @@ -31,7 +32,7 @@ runtime = ["kube-runtime"] deprecated-crd-v1beta1 = ["kube-core/deprecated-crd-v1beta1"] [package.metadata.docs.rs] -features = ["client", "native-tls", "rustls-tls", "derive", "ws", "oauth", "jsonpatch", "admission", "runtime", "k8s-openapi/v1_22"] +features = ["client", "native-tls", "rustls-tls", "openssl-tls", "derive", "ws", "oauth", "jsonpatch", "admission", "runtime", "k8s-openapi/v1_22"] # Define the configuration attribute `docsrs`. Used to enable `doc_cfg` feature. rustdoc-args = ["--cfg", "docsrs"]