Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rustls support #114

Merged
merged 11 commits into from
Feb 1, 2020
3 changes: 2 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ jobs:
- run: cargo build
- run: cargo test --lib
- run: cargo test --example crd_api crd_reflector
- run: cargo test --all-features -j4
- run: cargo test --features=openapi -j4
- run: cargo test --lib --no-default-features --features=rustls-tls
- save_cache:
paths:
- /usr/local/cargo/registry
Expand Down
31 changes: 17 additions & 14 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ authors = [
"clux <sszynrae@gmail.com>",
"ynqa <un.pensiero.vano@gmail.com>",
]
license-file = "LICENSE"
license = "Apache-2.0"
repository = "https://github.com/clux/kube-rs"
readme = "README.md"
keywords = ["kubernetes", "reflector", "informer", "client-go", "client-rust"]
Expand All @@ -21,7 +21,6 @@ serde = "1.0.104"
serde_derive = "1.0.104"
serde_json = "1.0.45"
serde_yaml = "0.8.11"
openssl = "0.10.26"
http = "0.2.0"
url = "2.1.1"
log = "0.4.8"
Expand All @@ -30,15 +29,25 @@ either = "1.5.3"
thiserror = "1.0.9"
futures-timer = "2.0.0"
futures = "0.3.1"

[features]
default = []
openapi = ["k8s-openapi"]
openssl = { version = "0.10.27", optional = true }
rustls = { version = "0.16.0", optional = true }

[dependencies.reqwest]
version = "0.10.1"
features = ["json", "gzip", "native-tls"]
# TODO: rustls
default-features = false
features = ["json", "gzip"]

[dependencies.k8s-openapi]
version = "0.7.1"
default-features = false
features = []
optional = true

[features]
default = ["native-tls"]
native-tls = ["openssl", "reqwest/native-tls"]
rustls-tls = ["rustls", "reqwest/rustls-tls"]
openapi = ["k8s-openapi"]

[dev-dependencies]
tempfile = "3.1.0"
Expand All @@ -50,9 +59,3 @@ anyhow = "1.0.26"
version = "0.7.1"
default-features = false
features = ["v1_15"]

[dependencies.k8s-openapi]
version = "0.7.1"
default-features = false
features = []
optional = true
19 changes: 13 additions & 6 deletions src/config/apis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,12 +155,19 @@ impl AuthInfo {
Some(provider) => {
if let Some(access_token) = provider.config.get("access-token") {
self.token = Some(access_token.clone());
if utils::is_expired(&provider.config["expiry"]) {
let client = oauth2::CredentialsClient::new()?;
let token = client.request_token(&[
"https://www.googleapis.com/auth/cloud-platform".to_string(),
]).await?;
self.token = Some(token.access_token);
#[cfg(feature = "native-tls")]
{ // TODO: allow rusttls with this auth provider bs
if utils::is_expired(&provider.config["expiry"]) {
let client = oauth2::CredentialsClient::new()?;
let token = client.request_token(&[
"https://www.googleapis.com/auth/cloud-platform".to_string(),
]).await?;
self.token = Some(token.access_token);
}
}
#[cfg(feature = "rustls-tls")]
{
error!("kube-rs does not support auth_provider setup with rustls atm")
}
}
if let Some(id_token) = provider.config.get("id-token") {
Expand Down
6 changes: 3 additions & 3 deletions src/config/incluster_config.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::env;

use crate::{Result, Error};
use openssl::x509::X509;
use reqwest::Certificate;

use crate::config::utils;

Expand Down Expand Up @@ -31,9 +31,9 @@ pub fn load_token() -> Result<String> {
}

/// Returns certification from specified path in cluster.
pub fn load_cert() -> Result<X509> {
pub fn load_cert() -> Result<Certificate> {
let ca = utils::data_or_file_with_base64(&None, &Some(SERVICE_CERTFILE))?;
X509::from_pem(&ca).map_err(|e| Error::KubeConfig(format!("{}", e)))
Certificate::from_pem(&ca).map_err(|e| Error::KubeConfig(format!("{}", e)))
}

/// Returns the default namespace from specified path in cluster.
Expand Down
75 changes: 67 additions & 8 deletions src/config/kube_config.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,29 @@
use std::path::Path;

#[cfg(feature = "native-tls")]
use openssl::{
pkcs12::Pkcs12,
pkey::PKey,
x509::X509,
};

use reqwest::{Identity, Certificate};

use crate::{Error, Result};
use crate::config::apis::{AuthInfo, Cluster, Config, Context};

/// Regardless of tls type, a Certificate Der is always a byte array
pub struct Der(pub Vec<u8>);

use std::convert::TryFrom;
impl TryFrom<Der> for Certificate {
type Error = Error;
fn try_from(val: Der) -> Result<Certificate> {
Certificate::from_der(&val.0)
.map_err(Error::ReqwestError)
}
}

/// KubeConfigLoader loads current context, cluster, and authentication information.
#[derive(Clone,Debug)]
pub struct KubeConfigLoader {
Expand Down Expand Up @@ -39,7 +56,6 @@ impl KubeConfigLoader {
.ok_or_else(|| Error::KubeConfig("Unable to load cluster of context".into()))?;
let user_name = user.as_ref().unwrap_or(&current_context.user);

// NB: can't have async closures yet
let mut user_opt = None;
for named_user in config.auth_infos {
if &named_user.name == user_name {
Expand All @@ -56,20 +72,63 @@ impl KubeConfigLoader {
})
}

pub fn p12(&self, password: &str) -> Result<Pkcs12> {
#[cfg(feature="native-tls")]
pub fn identity(&self, password: &str) -> Result<Identity> {
let client_cert = &self.user.load_client_certificate()?;
let client_key = &self.user.load_client_key()?;

let x509 = X509::from_pem(&client_cert).map_err(|e| Error::SslError(format!("{}", e)))?;
let pkey = PKey::private_key_from_pem(&client_key).map_err(|e| Error::SslError(format!("{}", e)))?;
let x509 = X509::from_pem(&client_cert)
.map_err(|e| Error::SslError(format!("{}", e)))?;
let pkey = PKey::private_key_from_pem(&client_key)
.map_err(|e| Error::SslError(format!("{}", e)))?;

Pkcs12::builder()
let p12 = Pkcs12::builder()
.build(password, "kubeconfig", &pkey, &x509)
.map_err(|e| Error::SslError(format!("{}", e)))?;

let der = p12.to_der()
.map_err(|e| Error::SslError(format!("{}", e)))?;
Ok(Identity::from_pkcs12_der(&der, password)?)
}

#[cfg(feature="rustls-tls")]
pub fn identity(&self, _password: &str) -> Result<Identity> {
let client_cert = &self.user.load_client_certificate()?;
let client_key = &self.user.load_client_key()?;

let mut buffer = client_key.clone();
buffer.extend_from_slice(client_cert);
Identity::from_pem(&buffer.as_slice())
.map_err(|e| Error::SslError(format!("{}", e)))
}

pub fn ca_bundle(&self) -> Option<Result<Vec<X509>>> {
let bundle = self.cluster.load_certificate_authority().ok()?;
Some(X509::stack_from_pem(&bundle).map_err(|e| Error::SslError(format!("{}", e))))
#[cfg(feature="native-tls")]
pub fn ca_bundle(&self) -> Result<Vec<Der>> {
let bundle = self.cluster.load_certificate_authority()
.map_err(|e| Error::SslError(format!("{}", e)))?;
let bundle = X509::stack_from_pem(&bundle)
.map_err(|e| Error::SslError(format!("{}", e)))?;

let mut stack = vec![];
for ca in bundle {
let der = ca.to_der().map_err(|e| Error::SslError(format!("{}", e)))?;
stack.push(Der(der))
}
Ok(stack)
}

#[cfg(feature = "rustls-tls")]
pub fn ca_bundle(&self) -> Result<Vec<Der>> {
use rustls::internal::pemfile;
use std::io::Cursor;
let bundle = self.cluster.load_certificate_authority()?;
let mut pem = Cursor::new(bundle);
pem.set_position(0);

let mut stack = vec![];
for ca in pemfile::certs(&mut pem).map_err(|e| Error::SslError(format!("{:?}", e)))? {
stack.push(Der(ca.0))
}
Ok(stack)
}
}
48 changes: 21 additions & 27 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ mod incluster_config;
mod kube_config;
mod utils;

use crate::config::kube_config::{Der};
use crate::{Error, Result};
use std::convert::TryInto;
use base64;
use openssl::x509::X509;
use reqwest::{header, Certificate, Client, ClientBuilder, Identity};
use reqwest::{header, Client, ClientBuilder};

use self::kube_config::KubeConfigLoader;

Expand Down Expand Up @@ -66,8 +67,11 @@ pub async fn load_kube_config_with(options: ConfigOptions) -> Result<Configurati
))
}

#[cfg(target_os = "macos")]
fn platform_cfg_client_builder(client_builder: ClientBuilder, ca: &X509) -> ClientBuilder {
// temporary catalina hack for openssl only
#[cfg(all(target_os = "macos", feature="native-tls"))]
fn hacky_cert_lifetime_for_macos(client_builder: ClientBuilder, ca_: &Der) -> ClientBuilder {
use openssl::x509::X509;
let ca = X509::from_der(&ca_.0).expect("valid der is a der");
if ca
.not_before()
.diff(ca.not_after())
Expand All @@ -80,8 +84,8 @@ fn platform_cfg_client_builder(client_builder: ClientBuilder, ca: &X509) -> Clie
}
}

#[cfg(not(target_os = "macos"))]
fn platform_cfg_client_builder(client_builder: ClientBuilder, _: &X509) -> ClientBuilder {
#[cfg(any(not(target_os = "macos"), not(feature="native-tls")))]
fn hacky_cert_lifetime_for_macos(client_builder: ClientBuilder, _: &Der) -> ClientBuilder {
client_builder
}

Expand Down Expand Up @@ -112,22 +116,17 @@ pub async fn create_client_builder(options: ConfigOptions) -> Result<(ClientBuil

let mut client_builder = Client::builder();

if let Some(bundle) = loader.ca_bundle() {
for ca in bundle? {
let cert = Certificate::from_der(&ca.to_der().map_err(|e| Error::SslError(format!("{}", e)))?)
.map_err(Error::ReqwestError)?;
client_builder = client_builder.add_root_certificate(cert);
client_builder = platform_cfg_client_builder(client_builder, &ca);
}
for ca in loader.ca_bundle()? {
client_builder = hacky_cert_lifetime_for_macos(client_builder, &ca);
client_builder = client_builder.add_root_certificate(ca.try_into()?);
}
match loader.p12(" ") {
Ok(p12) => {
let der = p12.to_der().map_err(|e| Error::SslError(format!("{}", e)))?;
let req_p12 = Identity::from_pkcs12_der(&der, " ")
.map_err(Error::ReqwestError)?;
client_builder = client_builder.identity(req_p12);

match loader.identity(" ") {
Ok(id) => {
client_builder = client_builder.identity(id);
}
Err(_) => {
Err(e) => {
warn!("failed to load client identity from kube config: {}", e);
// last resort only if configs ask for it, and no client certs
if let Some(true) = loader.cluster.insecure_skip_tls_verify {
client_builder = client_builder.danger_accept_invalid_certs(true);
Expand Down Expand Up @@ -160,7 +159,6 @@ pub async fn create_client_builder(options: ConfigOptions) -> Result<(ClientBuil
}

Ok((client_builder.default_headers(headers), loader))

}

/// Returns a config which is used by clients within pods on kubernetes.
Expand All @@ -174,11 +172,7 @@ pub fn incluster_config() -> Result<Configuration> {
incluster_config::SERVICE_PORTENV
)))?;

let ca = incluster_config::load_cert()
.map_err(|e| Error::SslError(format!("{}", e)))?;
let der = ca.to_der().map_err(|e| Error::SslError(format!("{}", e)))?;
let req_ca = Certificate::from_der(&der)
.map_err(|e| Error::SslError(format!("{}", e)))?;
let cert = incluster_config::load_cert()?;

let token = incluster_config::load_token()
.map_err(|e| Error::KubeConfig(format!("Unable to load in cluster token: {}", e)))?;
Expand All @@ -195,7 +189,7 @@ pub fn incluster_config() -> Result<Configuration> {
);

let client_builder = Client::builder()
.add_root_certificate(req_ca)
.add_root_certificate(cert)
.default_headers(headers);

Ok(Configuration::with_default_ns(
Expand Down
15 changes: 11 additions & 4 deletions src/oauth2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ use std::path::PathBuf;

use chrono::Utc;
use crate::{Result, Error};
use openssl::pkey::{PKey, Private};
use openssl::sign::Signer;
use openssl::rsa::Padding;
use openssl::hash::MessageDigest;

#[cfg(feature = "native-tls")]
use openssl::{
pkey::{PKey, Private},
sign::Signer,
rsa::Padding,
hash::MessageDigest,
};
use reqwest::Client;
use reqwest::header::CONTENT_TYPE;
//use time::Duration;
Expand Down Expand Up @@ -117,6 +121,8 @@ impl CredentialsClient {
client: Client::new(),
})
}

#[cfg(feature = "native-tls")]
pub async fn request_token(&self, scopes: &[String]) -> Result<Token> {
let private_key = PKey::private_key_from_pem(&self.credentials.private_key.as_bytes())
.map_err(|e| Error::SslError(format!("{}", e)))?;
Expand Down Expand Up @@ -146,6 +152,7 @@ impl CredentialsClient {
Ok(token_response.into_token())
}

#[cfg(feature = "native-tls")]
fn jws_encode(&self, claim: &Claim, header: &Header, key: PKey<Private>) -> Result<String> {
let encoded_header = self.base64_encode(serde_json::to_string(&header).unwrap().as_bytes());
let encoded_claims = self.base64_encode(serde_json::to_string(&claim).unwrap().as_bytes());
Expand Down