Skip to content

Commit

Permalink
Expose methods to configure TLS connection
Browse files Browse the repository at this point in the history
- 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 kube-rs#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
  • Loading branch information
kazk committed May 31, 2021
1 parent 870e6ad commit 75077fe
Show file tree
Hide file tree
Showing 11 changed files with 207 additions and 223 deletions.
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -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"] }
```

Expand Down
6 changes: 3 additions & 3 deletions examples/Cargo.toml
Expand Up @@ -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]
Expand All @@ -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"
Expand Down
7 changes: 1 addition & 6 deletions kube-runtime/Cargo.toml
Expand Up @@ -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"
Expand All @@ -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"}
Expand Down
1 change: 0 additions & 1 deletion kube/Cargo.toml
Expand Up @@ -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 }
Expand Down
28 changes: 22 additions & 6 deletions kube/src/client/mod.rs
Expand Up @@ -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;
Expand All @@ -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,
};

Expand Down Expand Up @@ -423,8 +422,25 @@ impl TryFrom<Config> 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.
Expand Down
14 changes: 7 additions & 7 deletions kube/src/config/mod.rs
Expand Up @@ -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};
Expand Down Expand Up @@ -35,9 +36,9 @@ pub struct Config {
pub timeout: Option<std::time::Duration>,
/// 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<u8>, String)>,
// TODO should keep client key and certificate separate. It's split later anyway.
/// Client certificate and private key in PEM.
identity_pem: Option<Vec<u8>>,
/// Stores information to tell the cluster who you are.
pub(crate) auth_info: AuthInfo,
}
Expand All @@ -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(),
}
}
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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,
})
}
Expand All @@ -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"))]
Expand Down
164 changes: 164 additions & 0 deletions 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<tokio_native_tls::native_tls::TlsConnector> {
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<rustls::ClientConfig> {
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<u8>>,
root_cert: Option<&Vec<Vec<u8>>>,
accept_invalid: bool,
) -> Result<TlsConnector> {
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<Vec<u8>> {
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<u8>>,
root_cert: Option<&Vec<Vec<u8>>>,
accept_invalid: bool,
) -> Result<ClientConfig> {
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<ServerCertVerified, rustls::TLSError> {
Ok(ServerCertVerified::assertion())
}
}
}
18 changes: 0 additions & 18 deletions kube/src/lib.rs
Expand Up @@ -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)*) => {
$(
Expand Down
14 changes: 9 additions & 5 deletions 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,
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 75077fe

Please sign in to comment.