From b9349910d5412232b512b25d1c8358c1c2f20a26 Mon Sep 17 00:00:00 2001 From: kazk Date: Tue, 25 May 2021 01:38:22 -0700 Subject: [PATCH] Expose methods to configure TLS connection - Add `Config::native_tls_connector` and `Config::rustls_client_config` - Remove the requirement of having `native-tls` or `rustls-tls` enabled when `client` is enabled. Allow one, both or none. - When both, the default Service will use `native-tls` because of #153. `rustls` can be still used with a custom client. Users will have an option to configure TLS at runtime. - When none, HTTP connector is used. - Note that `oauth` feature still requires tls feature. - Remove tls features from kube-runtime --- README.md | 2 +- examples/Cargo.toml | 6 +- kube-runtime/Cargo.toml | 7 +- kube/Cargo.toml | 1 - kube/src/client/mod.rs | 28 ++++-- kube/src/config/mod.rs | 14 +-- kube/src/config/tls.rs | 164 +++++++++++++++++++++++++++++++ kube/src/lib.rs | 18 ---- kube/src/service/auth/oauth.rs | 14 ++- kube/src/service/mod.rs | 2 - kube/src/service/tls.rs | 174 --------------------------------- 11 files changed, 207 insertions(+), 223 deletions(-) create mode 100644 kube/src/config/tls.rs delete mode 100644 kube/src/service/tls.rs diff --git a/README.md b/README.md index 8c08193dd..9eb941945 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,7 @@ Kube has basic support ([with caveats](https://github.com/clux/kube-rs/issues?q= ```toml [dependencies] kube = { version = "0.55.0", default-features = false, features = ["rustls-tls"] } -kube-runtime = { version = "0.55.0", default-features = false, features = ["rustls-tls"] } +kube-runtime = { version = "0.55.0" } k8s-openapi = { version = "0.11.0", default-features = false, features = ["v1_20"] } ``` diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 4867731b0..453de1a06 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -13,8 +13,8 @@ edition = "2018" default = ["native-tls", "schema", "kubederive", "ws"] kubederive = ["kube/derive"] # by default import kube-derive with its default features schema = ["kube-derive/schema"] # crd_derive_no_schema shows how to opt out -native-tls = ["kube/native-tls", "kube-runtime/native-tls"] -rustls-tls = ["kube/rustls-tls", "kube-runtime/rustls-tls"] +native-tls = ["kube/native-tls"] +rustls-tls = ["kube/rustls-tls"] ws = ["kube/ws"] [dev-dependencies] @@ -23,7 +23,7 @@ env_logger = "0.8.2" futures = "0.3.8" kube = { path = "../kube", version = "^0.55.0", default-features = false, features = ["admission"] } kube-derive = { path = "../kube-derive", version = "^0.55.0", default-features = false } # only needed to opt out of schema -kube-runtime = { path = "../kube-runtime", version = "^0.55.0", default-features = false } +kube-runtime = { path = "../kube-runtime", version = "^0.55.0" } kube-core = { path = "../kube-core", version = "^0.55.0", default-features = false } k8s-openapi = { version = "0.11.0", features = ["v1_20"], default-features = false } log = "0.4.11" diff --git a/kube-runtime/Cargo.toml b/kube-runtime/Cargo.toml index 130175d45..27c7fe9f9 100644 --- a/kube-runtime/Cargo.toml +++ b/kube-runtime/Cargo.toml @@ -14,7 +14,7 @@ edition = "2018" [dependencies] futures = "0.3.8" -kube = { path = "../kube", version = "^0.55.0", default-features = false } +kube = { path = "../kube", version = "^0.55.0", default-features = false, features = ["client"] } derivative = "2.1.1" serde = "1.0.118" smallvec = "1.6.0" @@ -28,11 +28,6 @@ tokio-util = { version = "0.6.0", features = ["time"] } version = "0.11.0" default-features = false -[features] -default = ["native-tls"] -native-tls = ["kube/native-tls"] -rustls-tls = ["kube/rustls-tls"] - [dev-dependencies] kube-derive = { path = "../kube-derive", version = "^0.55.0"} kube-core = { path = "../kube-core", version = "^0.55.0"} diff --git a/kube/Cargo.toml b/kube/Cargo.toml index 98a8fb1ce..3bbd4bce1 100644 --- a/kube/Cargo.toml +++ b/kube/Cargo.toml @@ -54,7 +54,6 @@ tokio-native-tls = { version = "0.3.0", optional = true } tokio-rustls = { version = "0.22.0", features = ["dangerous_configuration"], optional = true } bytes = { version = "1.0.0", optional = true } tokio = { version = "1.0.1", features = ["time", "signal", "sync"], optional = true } -static_assertions = "1.1.0" kube-derive = { path = "../kube-derive", version = "^0.55.0", optional = true } kube-core = { path = "../kube-core", version = "^0.55.0"} jsonpath_lib = { version = "0.2.6", optional = true } diff --git a/kube/src/client/mod.rs b/kube/src/client/mod.rs index b0a28d05c..996e3782f 100644 --- a/kube/src/client/mod.rs +++ b/kube/src/client/mod.rs @@ -8,13 +8,13 @@ //! The [`Client`] can also be used with [`Discovery`](crate::Discovery) to dynamically //! retrieve the resources served by the kubernetes API. -use std::convert::{TryFrom, TryInto}; +use std::convert::TryFrom; use bytes::Bytes; use either::{Either, Left, Right}; use futures::{self, Stream, StreamExt, TryStream, TryStreamExt}; use http::{self, HeaderValue, Request, Response, StatusCode}; -use hyper::Body; +use hyper::{client::HttpConnector, Body}; use hyper_timeout::TimeoutConnector; use k8s_openapi::apimachinery::pkg::apis::meta::v1 as k8s_meta_v1; pub use kube_core::response::Status; @@ -28,13 +28,12 @@ use tokio_util::{ }; use tower::{buffer::Buffer, util::BoxService, BoxError, Service, ServiceBuilder, ServiceExt}; - #[cfg(feature = "gzip")] use crate::service::{accept_compressed, maybe_decompress}; use crate::{ api::WatchEvent, error::{ConfigError, ErrorResponse}, - service::{set_cluster_url, set_default_headers, AuthLayer, Authentication, HttpsConnector, LogRequest}, + service::{set_cluster_url, set_default_headers, AuthLayer, Authentication, LogRequest}, Config, Error, Result, }; @@ -423,8 +422,25 @@ impl TryFrom for Client { .map_response(maybe_decompress) .into_inner(); - let https: HttpsConnector<_> = config.try_into()?; - let mut connector = TimeoutConnector::new(https); + 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")] + 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"))] + let connector = hyper_rustls::HttpsConnector::from(( + connector, + std::sync::Arc::new(config.rustls_tls_client_config()?), + )); + + let mut connector = TimeoutConnector::new(connector); if let Some(timeout) = timeout { // reqwest's timeout is applied from when the request stars connecting until // the response body has finished. diff --git a/kube/src/config/mod.rs b/kube/src/config/mod.rs index 72a6d1047..6b37b0aaf 100644 --- a/kube/src/config/mod.rs +++ b/kube/src/config/mod.rs @@ -7,6 +7,7 @@ mod file_config; mod file_loader; mod incluster_config; +#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] mod tls; mod utils; use crate::{error::ConfigError, Result}; @@ -35,9 +36,9 @@ pub struct Config { pub timeout: Option, /// Whether to accept invalid ceritifacts pub accept_invalid_certs: bool, - /// Client certs and key in PEM format and a password for a client to create `Identity` with. - /// Password is only used with `native_tls` to create a PKCS12 archive. - pub(crate) identity: Option<(Vec, String)>, + // TODO should keep client key and certificate separate. It's split later anyway. + /// Client certificate and private key in PEM. + identity_pem: Option>, /// Stores information to tell the cluster who you are. pub(crate) auth_info: AuthInfo, } @@ -56,7 +57,7 @@ impl Config { headers: HeaderMap::new(), timeout: Some(DEFAULT_TIMEOUT), accept_invalid_certs: false, - identity: None, + identity_pem: None, auth_info: AuthInfo::default(), } } @@ -114,7 +115,7 @@ impl Config { headers: HeaderMap::new(), timeout: Some(DEFAULT_TIMEOUT), accept_invalid_certs: false, - identity: None, + identity_pem: None, auth_info: AuthInfo { token: Some(token), ..Default::default() @@ -180,7 +181,7 @@ impl Config { headers: HeaderMap::new(), timeout: Some(DEFAULT_TIMEOUT), accept_invalid_certs, - identity: identity_pem.map(|i| (i, String::from(IDENTITY_PASSWORD))), + identity_pem, auth_info: loader.user, }) } @@ -189,7 +190,6 @@ impl Config { // https://github.com/clux/kube-rs/issues/146#issuecomment-590924397 /// Default Timeout const DEFAULT_TIMEOUT: Duration = Duration::from_secs(295); -const IDENTITY_PASSWORD: &str = " "; // temporary catalina hack for openssl only #[cfg(all(target_os = "macos", feature = "native-tls"))] diff --git a/kube/src/config/tls.rs b/kube/src/config/tls.rs new file mode 100644 index 000000000..ce33326f7 --- /dev/null +++ b/kube/src/config/tls.rs @@ -0,0 +1,164 @@ +use crate::Result; + +use super::Config; + +impl Config { + /// Create `native_tls::TlsConnector` + #[cfg(feature = "native-tls")] + pub fn native_tls_connector(&self) -> Result { + self::native_tls::native_tls_connector( + self.identity_pem.as_ref(), + self.root_cert.as_ref(), + self.accept_invalid_certs, + ) + } + + /// Create `rustls::ClientConfig` + #[cfg(feature = "rustls-tls")] + pub fn rustls_tls_client_config(&self) -> Result { + self::rustls_tls::rustls_client_config( + self.identity_pem.as_ref(), + self.root_cert.as_ref(), + self.accept_invalid_certs, + ) + } +} + + +#[cfg(feature = "native-tls")] +mod native_tls { + use tokio_native_tls::native_tls::{Certificate, Identity, TlsConnector}; + + use crate::{Error, Result}; + + const IDENTITY_PASSWORD: &str = " "; + + /// Create `native_tls::TlsConnector`. + pub fn native_tls_connector( + identity_pem: Option<&Vec>, + root_cert: Option<&Vec>>, + accept_invalid: bool, + ) -> Result { + let mut builder = TlsConnector::builder(); + if let Some(pem) = identity_pem { + let identity = pkcs12_from_pem(pem, IDENTITY_PASSWORD)?; + builder.identity( + Identity::from_pkcs12(&identity, IDENTITY_PASSWORD) + .map_err(|e| Error::SslError(format!("{}", e)))?, + ); + } + + if let Some(ders) = root_cert { + for der in ders { + builder.add_root_certificate( + Certificate::from_der(&der).map_err(|e| Error::SslError(format!("{}", e)))?, + ); + } + } + + if accept_invalid { + builder.danger_accept_invalid_certs(true); + } + + let connector = builder.build().map_err(|e| Error::SslError(format!("{}", e)))?; + Ok(connector) + } + + // TODO Replace this with pure Rust implementation to avoid depending on openssl on macOS and Win + fn pkcs12_from_pem(pem: &[u8], password: &str) -> Result> { + use openssl::{pkcs12::Pkcs12, pkey::PKey, x509::X509}; + let x509 = X509::from_pem(&pem)?; + let pkey = PKey::private_key_from_pem(&pem)?; + let p12 = Pkcs12::builder().build(password, "kubeconfig", &pkey, &x509)?; + let der = p12.to_der()?; + Ok(der) + } +} + +#[cfg(feature = "rustls-tls")] +mod rustls_tls { + use std::sync::Arc; + + use tokio_rustls::{ + rustls::{self, Certificate, ClientConfig, ServerCertVerified, ServerCertVerifier}, + webpki::DNSNameRef, + }; + + use crate::{Error, Result}; + + /// Create `rustls::ClientConfig`. + pub fn rustls_client_config( + identity_pem: Option<&Vec>, + root_cert: Option<&Vec>>, + accept_invalid: bool, + ) -> Result { + use rustls::internal::pemfile; + use std::io::Cursor; + + // Based on code from `reqwest` + let mut client_config = ClientConfig::new(); + if let Some(buf) = identity_pem { + let (key, certs) = { + let mut pem = Cursor::new(buf); + let certs = pemfile::certs(&mut pem) + .map_err(|_| Error::SslError("No valid certificate was found".into()))?; + pem.set_position(0); + + let mut sk = pemfile::pkcs8_private_keys(&mut pem) + .and_then(|pkcs8_keys| { + if pkcs8_keys.is_empty() { + Err(()) + } else { + Ok(pkcs8_keys) + } + }) + .or_else(|_| { + pem.set_position(0); + pemfile::rsa_private_keys(&mut pem) + }) + .map_err(|_| Error::SslError("No valid private key was found".into()))?; + + if let (Some(sk), false) = (sk.pop(), certs.is_empty()) { + (sk, certs) + } else { + return Err(Error::SslError("private key or certificate not found".into())); + } + }; + + client_config + .set_single_client_cert(certs, key) + .map_err(|e| Error::SslError(format!("{}", e)))?; + } + + if let Some(ders) = root_cert { + for der in ders { + client_config + .root_store + .add(&Certificate(der.to_owned())) + .map_err(|e| Error::SslError(format!("{}", e)))?; + } + } + + if accept_invalid { + client_config + .dangerous() + .set_certificate_verifier(Arc::new(NoCertificateVerification {})); + } + + Ok(client_config) + } + + struct NoCertificateVerification {} + + impl ServerCertVerifier for NoCertificateVerification { + fn verify_server_cert( + &self, + _roots: &rustls::RootCertStore, + _presented_certs: &[rustls::Certificate], + _dns_name: DNSNameRef<'_>, + _ocsp: &[u8], + ) -> Result { + Ok(ServerCertVerified::assertion()) + } + } +} diff --git a/kube/src/lib.rs b/kube/src/lib.rs index 760ea275e..d3c065753 100644 --- a/kube/src/lib.rs +++ b/kube/src/lib.rs @@ -74,24 +74,6 @@ #![deny(missing_docs)] #![deny(unsafe_code)] -#[macro_use] extern crate static_assertions; -assert_cfg!( - not(all(feature = "native-tls", feature = "rustls-tls")), - "Must use exactly one of native-tls or rustls-tls features" -); -assert_cfg!( - any( - all(feature = "native-tls", feature = "client"), - all(feature = "rustls-tls", feature = "client"), - all( - not(feature = "rustls-tls"), - not(feature = "native-tls"), - not(feature = "client") - ), - ), - "You must use a tls stack when using the client feature" -); - macro_rules! cfg_client { ($($item:item)*) => { $( diff --git a/kube/src/service/auth/oauth.rs b/kube/src/service/auth/oauth.rs index 1b73c82a9..fdca550c6 100644 --- a/kube/src/service/auth/oauth.rs +++ b/kube/src/service/auth/oauth.rs @@ -1,7 +1,5 @@ use std::{env, path::PathBuf}; -#[cfg(feature = "rustls-tls")] use hyper_rustls::HttpsConnector; -#[cfg(feature = "native-tls")] use hyper_tls::HttpsConnector; use tame_oauth::{ gcp::{ServiceAccountAccess, ServiceAccountInfo, TokenOrRequest}, Token, @@ -52,10 +50,16 @@ impl Gcp { Ok(TokenOrRequest::Request { request, scope_hash, .. }) => { - #[cfg(feature = "native-tls")] - let https = HttpsConnector::new(); + #[cfg(not(any(feature = "native-tls", feature = "rustls-tls")))] + compile_error!( + "At least one of native-tls or rustls-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 = HttpsConnector::with_native_roots(); + let https = hyper_rustls::HttpsConnector::with_native_roots(); + #[cfg(all(not(feature = "rustls-tls"), feature = "native-tls"))] + let https = hyper_tls::HttpsConnector::new(); let client = hyper::Client::builder().build::<_, hyper::Body>(https); let res = client diff --git a/kube/src/service/mod.rs b/kube/src/service/mod.rs index 0a455a25c..02ea2023e 100644 --- a/kube/src/service/mod.rs +++ b/kube/src/service/mod.rs @@ -4,7 +4,6 @@ mod auth; #[cfg(feature = "gzip")] mod compression; mod headers; mod log; -mod tls; mod url; #[cfg(feature = "gzip")] @@ -13,6 +12,5 @@ pub(crate) use self::{ auth::{AuthLayer, Authentication}, headers::set_default_headers, log::LogRequest, - tls::HttpsConnector, url::set_cluster_url, }; diff --git a/kube/src/service/tls.rs b/kube/src/service/tls.rs deleted file mode 100644 index 5c26807a9..000000000 --- a/kube/src/service/tls.rs +++ /dev/null @@ -1,174 +0,0 @@ -// Create `HttpsConnector` from `Config`. -// - hyper_tls::HttpsConnector from (hyper::client::HttpConnector, tokio_native_tls::TlsConnector) -// - hyper_rustls::HttpsConnector from (hyper::client::HttpConnector, Arc) - -pub use connector::HttpsConnector; - -#[cfg(feature = "native-tls")] -mod connector { - use std::convert::{TryFrom, TryInto}; - - use hyper::client::HttpConnector; - use tokio_native_tls::native_tls::{Certificate, Identity, TlsConnector}; - - use crate::{Config, Error, Result}; - - pub use hyper_tls::HttpsConnector; - use tokio_native_tls::TlsConnector as AsyncTlsConnector; - - impl TryFrom for HttpsConnector { - type Error = Error; - - fn try_from(config: Config) -> Result { - let mut http = HttpConnector::new(); - http.enforce_http(false); - let tls: AsyncTlsConnector = config.try_into()?; - Ok(HttpsConnector::from((http, tls))) - } - } - - impl TryFrom for AsyncTlsConnector { - type Error = Error; - - fn try_from(config: Config) -> Result { - let mut builder = TlsConnector::builder(); - if let Some((pem, identity_password)) = config.identity.as_ref() { - let identity = pkcs12_from_pem(pem, identity_password)?; - builder.identity( - Identity::from_pkcs12(&identity, identity_password) - .map_err(|e| Error::SslError(format!("{}", e)))?, - ); - } - - if let Some(ders) = config.root_cert { - for der in ders { - builder.add_root_certificate( - Certificate::from_der(&der).map_err(|e| Error::SslError(format!("{}", e)))?, - ); - } - } - - if config.accept_invalid_certs { - builder.danger_accept_invalid_certs(config.accept_invalid_certs); - } - - let connector = builder.build().map_err(|e| Error::SslError(format!("{}", e)))?; - Ok(AsyncTlsConnector::from(connector)) - } - } - - fn pkcs12_from_pem(pem: &[u8], password: &str) -> Result> { - use openssl::{pkcs12::Pkcs12, pkey::PKey, x509::X509}; - let x509 = X509::from_pem(&pem)?; - let pkey = PKey::private_key_from_pem(&pem)?; - let p12 = Pkcs12::builder().build(password, "kubeconfig", &pkey, &x509)?; - let der = p12.to_der()?; - Ok(der) - } -} - -#[cfg(feature = "rustls-tls")] -mod connector { - use std::{ - convert::{TryFrom, TryInto}, - sync::Arc, - }; - - use hyper::client::HttpConnector; - use tokio_rustls::{ - rustls::{self, Certificate, ClientConfig, ServerCertVerified, ServerCertVerifier}, - webpki::DNSNameRef, - }; - - use crate::{config::Config, Error, Result}; - - pub use hyper_rustls::HttpsConnector; - - impl TryFrom for HttpsConnector { - type Error = Error; - - fn try_from(config: Config) -> Result { - let mut http = HttpConnector::new(); - http.enforce_http(false); - let client_config: ClientConfig = config.try_into()?; - let client_config = Arc::new(client_config); - - Ok(HttpsConnector::from((http, client_config))) - } - } - - impl TryFrom for ClientConfig { - type Error = Error; - - fn try_from(config: Config) -> Result { - use rustls::internal::pemfile; - use std::io::Cursor; - - // Based on code from `reqwest` - let mut client_config = ClientConfig::new(); - if let Some((buf, _)) = config.identity.as_ref() { - let (key, certs) = { - let mut pem = Cursor::new(buf); - let certs = pemfile::certs(&mut pem) - .map_err(|_| Error::SslError("No valid certificate was found".into()))?; - pem.set_position(0); - - let mut sk = pemfile::pkcs8_private_keys(&mut pem) - .and_then(|pkcs8_keys| { - if pkcs8_keys.is_empty() { - Err(()) - } else { - Ok(pkcs8_keys) - } - }) - .or_else(|_| { - pem.set_position(0); - pemfile::rsa_private_keys(&mut pem) - }) - .map_err(|_| Error::SslError("No valid private key was found".into()))?; - - if let (Some(sk), false) = (sk.pop(), certs.is_empty()) { - (sk, certs) - } else { - return Err(Error::SslError("private key or certificate not found".into())); - } - }; - - client_config - .set_single_client_cert(certs, key) - .map_err(|e| Error::SslError(format!("{}", e)))?; - } - - if let Some(ders) = config.root_cert { - for der in ders { - client_config - .root_store - .add(&Certificate(der)) - .map_err(|e| Error::SslError(format!("{}", e)))?; - } - } - - if config.accept_invalid_certs { - client_config - .dangerous() - .set_certificate_verifier(Arc::new(NoCertificateVerification {})); - } - - Ok(client_config) - } - } - - struct NoCertificateVerification {} - - impl ServerCertVerifier for NoCertificateVerification { - fn verify_server_cert( - &self, - _roots: &rustls::RootCertStore, - _presented_certs: &[rustls::Certificate], - _dns_name: DNSNameRef<'_>, - _ocsp: &[u8], - ) -> Result { - Ok(ServerCertVerified::assertion()) - } - } -}