Skip to main content

kube_client/client/
tls.rs

1#[cfg(feature = "rustls-tls")]
2pub mod rustls_tls {
3    use std::{
4        path::PathBuf,
5        sync::{Arc, RwLock},
6        time::{Duration, Instant},
7    };
8
9    // Only the webpki-roots fallback uses this; the default platform-verifier path does not.
10    #[cfg(feature = "webpki-roots")]
11    use hyper_rustls::ConfigBuilderExt;
12    use rustls::{
13        self, ClientConfig, DigitallySignedStruct, RootCertStore,
14        client::{
15            WebPkiServerVerifier,
16            danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
17        },
18        pki_types::{CertificateDer, InvalidDnsNameError, PrivateKeyDer, ServerName},
19    };
20    use thiserror::Error;
21
22    /// Errors from Rustls
23    #[derive(Debug, Error)]
24    pub enum Error {
25        /// Identity PEM is invalid
26        #[error("identity PEM is invalid: {0}")]
27        InvalidIdentityPem(#[source] rustls::pki_types::pem::Error),
28
29        /// Identity PEM is missing a private key: the key must be PKCS8 or RSA/PKCS1
30        #[error("identity PEM is missing a private key: the key must be PKCS8 or RSA/PKCS1")]
31        MissingPrivateKey,
32
33        /// Identity PEM is missing certificate
34        #[error("identity PEM is missing certificate")]
35        MissingCertificate,
36
37        /// Invalid private key
38        #[error("invalid private key: {0}")]
39        InvalidPrivateKey(#[source] rustls::Error),
40
41        /// Unknown private key format
42        #[error("unknown private key format")]
43        UnknownPrivateKeyFormat,
44
45        // Using type-erased error to avoid depending on webpki
46        /// Failed to add a root certificate
47        #[error("failed to add a root certificate: {0}")]
48        AddRootCertificate(#[source] Box<dyn std::error::Error + Send + Sync>),
49
50        /// No valid native root CA certificates found
51        #[error("no valid native root CA certificates found: {0}")]
52        NoValidNativeRootCA(#[source] std::io::Error),
53
54        /// Invalid server name
55        #[error("invalid server name: {0}")]
56        InvalidServerName(#[source] InvalidDnsNameError),
57    }
58
59    /// Create `rustls::ClientConfig`.
60    pub fn rustls_client_config(
61        identity_pem: Option<&[u8]>,
62        root_certs: Option<&[Vec<u8>]>,
63        accept_invalid: bool,
64    ) -> Result<ClientConfig, Error> {
65        let config_builder = if let Some(certs) = root_certs {
66            ClientConfig::builder().with_root_certificates(root_store(certs)?)
67        } else {
68            // No CA was configured (e.g. the kubeconfig has no `certificate-authority`),
69            // so defer to the trust store, mirroring how client-go leaves `RootCAs`
70            // nil to use the OS roots.
71            #[cfg(feature = "webpki-roots")]
72            {
73                // Opt-in override: use the bundled WebPKI (Mozilla) roots.
74                ClientConfig::builder().with_webpki_roots()
75            }
76            #[cfg(not(feature = "webpki-roots"))]
77            {
78                // Default: delegate to the OS-native verifier (macOS Security
79                // framework, Windows CryptoAPI, native roots on Linux/Android/iOS),
80                // honouring OS trust policies such as per-certificate trust settings.
81                use rustls_platform_verifier::BuilderVerifierExt;
82                ClientConfig::builder()
83                    .with_platform_verifier()
84                    // Reuse the type-erased trust-setup error variant to keep the
85                    // public `Error` enum stable.
86                    .map_err(|e| Error::AddRootCertificate(Box::new(e)))?
87            }
88        };
89
90        let mut client_config = if let Some((chain, pkey)) = identity_pem.map(client_auth).transpose()? {
91            config_builder
92                .with_client_auth_cert(chain, pkey)
93                .map_err(Error::InvalidPrivateKey)?
94        } else {
95            config_builder.with_no_client_auth()
96        };
97
98        if accept_invalid {
99            client_config
100                .dangerous()
101                .set_certificate_verifier(std::sync::Arc::new(NoCertificateVerification {}));
102        }
103        Ok(client_config)
104    }
105
106    fn root_store(root_certs: &[Vec<u8>]) -> Result<rustls::RootCertStore, Error> {
107        let mut root_store = rustls::RootCertStore::empty();
108        for der in root_certs {
109            root_store
110                .add(CertificateDer::from(der.to_owned()))
111                .map_err(|e| Error::AddRootCertificate(Box::new(e)))?;
112        }
113        Ok(root_store)
114    }
115
116    /// A [`ServerCertVerifier`] that re-reads the CA bundle file roughly once
117    /// per minute to pick up CA rotation.
118    ///
119    /// This mirrors the token-file reload behaviour in
120    /// [`crate::client::auth::TokenFile`]: the service-account `ca.crt` lives
121    /// in the same projected volume and rotates under the same mechanism.
122    /// Existing TLS connections are unaffected (they already handshook); new
123    /// connections use the freshly loaded roots.
124    ///
125    /// If a reload fails (file missing, parse error), the last successfully
126    /// loaded verifier is retained — same policy as `TokenFile`, per
127    /// <https://github.com/kubernetes/kubernetes/issues/68164>.
128    #[derive(Debug)]
129    pub(crate) struct ReloadingVerifier {
130        path: PathBuf,
131        inner: RwLock<(Arc<WebPkiServerVerifier>, Instant)>,
132    }
133
134    impl ReloadingVerifier {
135        const RELOAD_INTERVAL: Duration = Duration::from_secs(60);
136
137        pub(crate) fn new(path: PathBuf) -> Result<Self, Error> {
138            let verifier = Self::load(&path)?;
139            Ok(Self {
140                path,
141                inner: RwLock::new((verifier, Instant::now())),
142            })
143        }
144
145        fn load(path: &PathBuf) -> Result<Arc<WebPkiServerVerifier>, Error> {
146            let pem = std::fs::read(path).map_err(|e| Error::AddRootCertificate(Box::new(e)))?;
147            let ders = crate::config::certs(&pem).map_err(|e| Error::AddRootCertificate(Box::new(e)))?;
148            let mut store = RootCertStore::empty();
149            for der in ders {
150                store
151                    .add(CertificateDer::from(der))
152                    .map_err(|e| Error::AddRootCertificate(Box::new(e)))?;
153            }
154            WebPkiServerVerifier::builder(Arc::new(store))
155                .build()
156                .map_err(|e| Error::AddRootCertificate(Box::new(e)))
157        }
158
159        fn current(&self) -> Arc<WebPkiServerVerifier> {
160            {
161                let guard = self.inner.read().unwrap_or_else(|e| e.into_inner());
162                if guard.1.elapsed() < Self::RELOAD_INTERVAL {
163                    return guard.0.clone();
164                }
165            }
166            let mut guard = self.inner.write().unwrap_or_else(|e| e.into_inner());
167            if guard.1.elapsed() < Self::RELOAD_INTERVAL {
168                return guard.0.clone();
169            }
170            if let Ok(fresh) = Self::load(&self.path) {
171                guard.0 = fresh;
172            } else {
173                tracing::warn!(path = ?self.path, "failed to reload CA bundle; keeping stale roots");
174            }
175            guard.1 = Instant::now();
176            guard.0.clone()
177        }
178    }
179
180    impl ServerCertVerifier for ReloadingVerifier {
181        fn verify_server_cert(
182            &self,
183            end_entity: &CertificateDer,
184            intermediates: &[CertificateDer],
185            server_name: &ServerName,
186            ocsp_response: &[u8],
187            now: rustls::pki_types::UnixTime,
188        ) -> Result<ServerCertVerified, rustls::Error> {
189            self.current()
190                .verify_server_cert(end_entity, intermediates, server_name, ocsp_response, now)
191        }
192
193        fn verify_tls12_signature(
194            &self,
195            message: &[u8],
196            cert: &CertificateDer,
197            dss: &DigitallySignedStruct,
198        ) -> Result<HandshakeSignatureValid, rustls::Error> {
199            self.current().verify_tls12_signature(message, cert, dss)
200        }
201
202        fn verify_tls13_signature(
203            &self,
204            message: &[u8],
205            cert: &CertificateDer,
206            dss: &DigitallySignedStruct,
207        ) -> Result<HandshakeSignatureValid, rustls::Error> {
208            self.current().verify_tls13_signature(message, cert, dss)
209        }
210
211        fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
212            self.current().supported_verify_schemes()
213        }
214    }
215
216    #[cfg(test)]
217    mod tests {
218        use super::*;
219
220        // EC P-256 self-signed CAs, valid until 2126. Regenerate with:
221        //   openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
222        //     -keyout /dev/null -out ca.pem -days 36500 -subj "/CN=test-ca-N"
223        const CA1: &str = "-----BEGIN CERTIFICATE-----
224MIIBgDCCASWgAwIBAgIUVrQf5d//S01a0fbXxYRIx9wc0VQwCgYIKoZIzj0EAwIw
225FDESMBAGA1UEAwwJdGVzdC1jYS0xMCAXDTI2MDMwNDEzMDk1MFoYDzIxMjYwMjA4
226MTMwOTUwWjAUMRIwEAYDVQQDDAl0ZXN0LWNhLTEwWTATBgcqhkjOPQIBBggqhkjO
227PQMBBwNCAARg57mWJPDsAIEQAgXqMOOfjMQP+PE9HqcZobycO8z94r/uRuV0wKx/
2280SvMsKFtnreut0bjgFtmZaWY+6d87Is9o1MwUTAdBgNVHQ4EFgQUjtGuhkM7LtHB
229gMPCJIxMwbY69OQwHwYDVR0jBBgwFoAUjtGuhkM7LtHBgMPCJIxMwbY69OQwDwYD
230VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgNJADBGAiEAj/WzNVJDg/cBtLqQVM77
231tkB+QyIXLG3Vi9Xj1YfW9QECIQDDFW8yFtgLeCg2Zhr4xQNq3/24r/01kI2rjFPO
232xBkDMw==
233-----END CERTIFICATE-----
234";
235        const CA2: &str = "-----BEGIN CERTIFICATE-----
236MIIBfjCCASWgAwIBAgIUZ7Qsiwan2joRz01p25/cy1XNNiwwCgYIKoZIzj0EAwIw
237FDESMBAGA1UEAwwJdGVzdC1jYS0yMCAXDTI2MDMwNDEzMDk1MFoYDzIxMjYwMjA4
238MTMwOTUwWjAUMRIwEAYDVQQDDAl0ZXN0LWNhLTIwWTATBgcqhkjOPQIBBggqhkjO
239PQMBBwNCAARJle2/yiOD5zp0UkjZg9Yy6ZHBItTLrqv/uzB2YMQg03frnqEUMzSV
240mFinosBcGpX/dPGfHNPhBMOpHmlocZu9o1MwUTAdBgNVHQ4EFgQUsqG0hSGDYsz2
241eGIsLIwJnCR5SFIwHwYDVR0jBBgwFoAUsqG0hSGDYsz2eGIsLIwJnCR5SFIwDwYD
242VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgNHADBEAiApvLu9DIC3/K/+G9ooOm75
243a72Cjw62aM8NfPe7ILs8SgIgL0VHe6ksTyB176RECCm3MJVnlhOop6b1tNvxjrru
244FRU=
245-----END CERTIFICATE-----
246";
247
248        fn expire(v: &ReloadingVerifier) {
249            // Can't move Instant backwards; instead, reach past the guard.
250            // The test pokes private state the same way auth::tests::token_file does.
251            v.inner.write().unwrap().1 = Instant::now().checked_sub(Duration::from_secs(120)).unwrap();
252        }
253
254        #[test]
255        fn reloading_verifier() {
256            #[cfg(feature = "aws-lc-rs")]
257            let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
258
259            let file = tempfile::NamedTempFile::new().unwrap();
260            std::fs::write(file.path(), CA1).unwrap();
261
262            let verifier = ReloadingVerifier::new(file.path().to_path_buf()).unwrap();
263            let first = verifier.current();
264
265            // File changed but we're still within the reload interval: no reload.
266            std::fs::write(file.path(), CA2).unwrap();
267            assert!(Arc::ptr_eq(&verifier.current(), &first));
268
269            // Force expiry: reload picks up CA2.
270            expire(&verifier);
271            let second = verifier.current();
272            assert!(!Arc::ptr_eq(&second, &first));
273
274            // File gone, expired again: keep stale verifier.
275            drop(file);
276            expire(&verifier);
277            assert!(Arc::ptr_eq(&verifier.current(), &second));
278        }
279    }
280
281    #[cfg(all(test, not(feature = "webpki-roots")))]
282    mod platform_verifier_tests {
283        use super::*;
284
285        // With no kubeconfig CA, the client config must fall back to the OS-native
286        // verifier instead of producing an empty (reject-everything) trust store.
287        #[test]
288        fn no_ca_falls_back_to_platform_verifier() {
289            #[cfg(feature = "aws-lc-rs")]
290            let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
291
292            rustls_client_config(None, None, false).expect("platform verifier config should build");
293        }
294    }
295
296    fn client_auth(data: &[u8]) -> Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>), Error> {
297        use rustls::pki_types::pem::{self, SectionKind};
298
299        let mut cert_chain = Vec::new();
300        let mut pkcs8_key = None;
301        let mut pkcs1_key = None;
302        let mut sec1_key = None;
303        let mut reader = std::io::Cursor::new(data);
304        while let Some((kind, der)) = pem::from_buf(&mut reader).map_err(Error::InvalidIdentityPem)? {
305            match kind {
306                SectionKind::Certificate => cert_chain.push(der.into()),
307                SectionKind::PrivateKey => pkcs8_key = Some(PrivateKeyDer::Pkcs8(der.into())),
308                SectionKind::RsaPrivateKey => pkcs1_key = Some(PrivateKeyDer::Pkcs1(der.into())),
309                SectionKind::EcPrivateKey => sec1_key = Some(PrivateKeyDer::Sec1(der.into())),
310                _ => return Err(Error::UnknownPrivateKeyFormat),
311            }
312        }
313
314        let private_key = pkcs8_key
315            .or(pkcs1_key)
316            .or(sec1_key)
317            .ok_or(Error::MissingPrivateKey)?;
318        if cert_chain.is_empty() {
319            return Err(Error::MissingCertificate);
320        }
321        Ok((cert_chain, private_key))
322    }
323
324    #[derive(Debug)]
325    struct NoCertificateVerification {}
326
327    impl ServerCertVerifier for NoCertificateVerification {
328        fn verify_server_cert(
329            &self,
330            _end_entity: &CertificateDer,
331            _intermediates: &[CertificateDer],
332            _server_name: &ServerName,
333            _ocsp_response: &[u8],
334            _now: rustls::pki_types::UnixTime,
335        ) -> Result<ServerCertVerified, rustls::Error> {
336            tracing::warn!("Server cert bypassed");
337            Ok(ServerCertVerified::assertion())
338        }
339
340        fn verify_tls13_signature(
341            &self,
342            _message: &[u8],
343            _cert: &CertificateDer,
344            _dss: &DigitallySignedStruct,
345        ) -> Result<HandshakeSignatureValid, rustls::Error> {
346            Ok(HandshakeSignatureValid::assertion())
347        }
348
349        fn verify_tls12_signature(
350            &self,
351            _message: &[u8],
352            _cert: &CertificateDer,
353            _dss: &DigitallySignedStruct,
354        ) -> Result<HandshakeSignatureValid, rustls::Error> {
355            Ok(HandshakeSignatureValid::assertion())
356        }
357
358        fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
359            use rustls::SignatureScheme;
360            vec![
361                SignatureScheme::RSA_PKCS1_SHA1,
362                SignatureScheme::ECDSA_SHA1_Legacy,
363                SignatureScheme::RSA_PKCS1_SHA256,
364                SignatureScheme::ECDSA_NISTP256_SHA256,
365                SignatureScheme::RSA_PKCS1_SHA384,
366                SignatureScheme::ECDSA_NISTP384_SHA384,
367                SignatureScheme::RSA_PKCS1_SHA512,
368                SignatureScheme::ECDSA_NISTP521_SHA512,
369                SignatureScheme::RSA_PSS_SHA256,
370                SignatureScheme::RSA_PSS_SHA384,
371                SignatureScheme::RSA_PSS_SHA512,
372                SignatureScheme::ED25519,
373                SignatureScheme::ED448,
374            ]
375        }
376    }
377}
378
379#[cfg(feature = "openssl-tls")]
380pub mod openssl_tls {
381    use openssl::{
382        pkey::PKey,
383        ssl::{SslConnector, SslConnectorBuilder, SslMethod},
384        x509::X509,
385    };
386    use thiserror::Error;
387
388    /// Errors from OpenSSL TLS
389    #[derive(Debug, Error)]
390    pub enum Error {
391        /// Failed to create OpenSSL HTTPS connector
392        #[error("failed to create OpenSSL HTTPS connector: {0}")]
393        CreateHttpsConnector(#[source] openssl::error::ErrorStack),
394
395        /// Failed to create OpenSSL SSL connector
396        #[error("failed to create OpenSSL SSL connector: {0}")]
397        CreateSslConnector(#[source] SslConnectorError),
398    }
399
400    /// Errors from creating a `SslConnectorBuilder`
401    #[derive(Debug, Error)]
402    pub enum SslConnectorError {
403        /// Failed to build SslConnectorBuilder
404        #[error("failed to build SslConnectorBuilder: {0}")]
405        CreateBuilder(#[source] openssl::error::ErrorStack),
406
407        /// Failed to deserialize PEM-encoded chain of certificates
408        #[error("failed to deserialize PEM-encoded chain of certificates: {0}")]
409        DeserializeCertificateChain(#[source] openssl::error::ErrorStack),
410
411        /// Failed to deserialize PEM-encoded private key
412        #[error("failed to deserialize PEM-encoded private key: {0}")]
413        DeserializePrivateKey(#[source] openssl::error::ErrorStack),
414
415        /// Failed to set private key
416        #[error("failed to set private key: {0}")]
417        SetPrivateKey(#[source] openssl::error::ErrorStack),
418
419        /// Failed to get a leaf certificate, the certificate chain is empty
420        #[error("failed to get a leaf certificate, the certificate chain is empty")]
421        GetLeafCertificate,
422
423        /// Failed to set the leaf certificate
424        #[error("failed to set the leaf certificate: {0}")]
425        SetLeafCertificate(#[source] openssl::error::ErrorStack),
426
427        /// Failed to append a certificate to the chain
428        #[error("failed to append a certificate to the chain: {0}")]
429        AppendCertificate(#[source] openssl::error::ErrorStack),
430
431        /// Failed to deserialize DER-encoded root certificate
432        #[error("failed to deserialize DER-encoded root certificate: {0}")]
433        DeserializeRootCertificate(#[source] openssl::error::ErrorStack),
434
435        /// Failed to add a root certificate
436        #[error("failed to add a root certificate: {0}")]
437        AddRootCertificate(#[source] openssl::error::ErrorStack),
438    }
439
440    /// Create `openssl::ssl::SslConnectorBuilder` required for `hyper_openssl::HttpsConnector`.
441    pub fn ssl_connector_builder(
442        identity_pem: Option<&Vec<u8>>,
443        root_certs: Option<&Vec<Vec<u8>>>,
444    ) -> Result<SslConnectorBuilder, SslConnectorError> {
445        let mut builder =
446            SslConnector::builder(SslMethod::tls()).map_err(SslConnectorError::CreateBuilder)?;
447        if let Some(pem) = identity_pem {
448            let mut chain = X509::stack_from_pem(pem)
449                .map_err(SslConnectorError::DeserializeCertificateChain)?
450                .into_iter();
451            let leaf_cert = chain.next().ok_or(SslConnectorError::GetLeafCertificate)?;
452            builder
453                .set_certificate(&leaf_cert)
454                .map_err(SslConnectorError::SetLeafCertificate)?;
455            for cert in chain {
456                builder
457                    .add_extra_chain_cert(cert)
458                    .map_err(SslConnectorError::AppendCertificate)?;
459            }
460
461            let pkey = PKey::private_key_from_pem(pem).map_err(SslConnectorError::DeserializePrivateKey)?;
462            builder
463                .set_private_key(&pkey)
464                .map_err(SslConnectorError::SetPrivateKey)?;
465        }
466
467        if let Some(ders) = root_certs {
468            for der in ders {
469                let cert = X509::from_der(der).map_err(SslConnectorError::DeserializeRootCertificate)?;
470                builder
471                    .cert_store_mut()
472                    .add_cert(cert)
473                    .map_err(SslConnectorError::AddRootCertificate)?;
474            }
475        }
476
477        Ok(builder)
478    }
479}