@@ -3,6 +3,7 @@ use std::fs;
33use std:: path:: PathBuf ;
44
55use anyhow:: { Context as AnyhowContext , Result , bail} ;
6+ use base64:: prelude:: * ;
67use kube:: config:: {
78 AuthInfo , Cluster , Context as KubeContext , Kubeconfig , NamedAuthInfo , NamedCluster ,
89 NamedContext ,
@@ -183,10 +184,39 @@ fn normalize_add_cluster_request(mut request: AddClusterRequest) -> Result<AddCl
183184 if !( request. server . starts_with ( "https://" ) || request. server . starts_with ( "http://" ) ) {
184185 bail ! ( "API server URL must start with http:// or https://" ) ;
185186 }
187+ if request. insecure_skip_tls_verify && request. certificate_authority_data . is_some ( ) {
188+ bail ! ( "CA data cannot be used when TLS verification is skipped" ) ;
189+ }
190+ request. certificate_authority_data = request
191+ . certificate_authority_data
192+ . map ( normalize_certificate_authority_data)
193+ . transpose ( ) ?;
186194
187195 Ok ( request)
188196}
189197
198+ fn normalize_certificate_authority_data ( value : String ) -> Result < String > {
199+ if looks_like_pem_certificate ( & value) {
200+ return Ok ( BASE64_STANDARD . encode ( value. as_bytes ( ) ) ) ;
201+ }
202+
203+ let compact = value. split_whitespace ( ) . collect :: < String > ( ) ;
204+ let decoded = BASE64_STANDARD
205+ . decode ( compact. as_bytes ( ) )
206+ . context ( "CA data must be a PEM certificate or base64-encoded PEM certificate" ) ?;
207+ let decoded = std:: str:: from_utf8 ( & decoded)
208+ . context ( "CA data must decode to a PEM certificate, not raw DER bytes" ) ?;
209+ if !looks_like_pem_certificate ( decoded) {
210+ bail ! ( "CA data must decode to a PEM certificate" ) ;
211+ }
212+
213+ Ok ( compact)
214+ }
215+
216+ fn looks_like_pem_certificate ( value : & str ) -> bool {
217+ value. contains ( "-----BEGIN CERTIFICATE-----" ) && value. contains ( "-----END CERTIFICATE-----" )
218+ }
219+
190220fn kubeconfig_write_path ( ) -> Result < PathBuf > {
191221 if let Some ( value) = std:: env:: var_os ( "KUBECONFIG" ) {
192222 let paths = std:: env:: split_paths ( & value)
@@ -271,6 +301,7 @@ pub(crate) fn server_host(server: &str) -> String {
271301mod tests {
272302 use std:: sync:: Mutex ;
273303
304+ use base64:: prelude:: * ;
274305 use kube:: config:: {
275306 AuthInfo , Cluster , Context as KubeContext , Kubeconfig , NamedAuthInfo , NamedCluster ,
276307 NamedContext ,
@@ -340,6 +371,75 @@ mod tests {
340371 let _ = std:: fs:: remove_file ( & path) ;
341372 }
342373
374+ #[ test]
375+ fn add_token_cluster_rejects_ca_when_tls_verification_is_skipped ( ) {
376+ let _guard = KUBECONFIG_ENV_LOCK . lock ( ) . unwrap ( ) ;
377+ let path = test_kubeconfig_path ( "aetheris-kube-test-insecure-ca" ) ;
378+ unsafe {
379+ std:: env:: set_var ( "KUBECONFIG" , & path) ;
380+ }
381+
382+ let request = AddClusterRequest {
383+ context_name : String :: from ( "invalid-tls" ) ,
384+ server : String :: from ( "https://api.example.com:6443" ) ,
385+ bearer_token : String :: from ( "sha256~token" ) ,
386+ certificate_authority_data : Some ( test_ca_pem ( ) ) ,
387+ insecure_skip_tls_verify : true ,
388+ original_context_name : None ,
389+ } ;
390+ let error = KubeManager :: add_token_cluster ( request) . expect_err ( "request must be rejected" ) ;
391+
392+ assert ! (
393+ error
394+ . to_string( )
395+ . contains( "CA data cannot be used when TLS verification is skipped" )
396+ ) ;
397+
398+ unsafe {
399+ std:: env:: remove_var ( "KUBECONFIG" ) ;
400+ }
401+ let _ = std:: fs:: remove_file ( & path) ;
402+ }
403+
404+ #[ test]
405+ fn add_token_cluster_encodes_pem_ca_data ( ) {
406+ let _guard = KUBECONFIG_ENV_LOCK . lock ( ) . unwrap ( ) ;
407+ let path = test_kubeconfig_path ( "aetheris-kube-test-ca-pem" ) ;
408+ unsafe {
409+ std:: env:: set_var ( "KUBECONFIG" , & path) ;
410+ }
411+
412+ let ca = test_ca_pem ( ) ;
413+ let request = AddClusterRequest {
414+ context_name : String :: from ( "pem-ca" ) ,
415+ server : String :: from ( "https://api.example.com:6443" ) ,
416+ bearer_token : String :: from ( "sha256~token" ) ,
417+ certificate_authority_data : Some ( ca. clone ( ) ) ,
418+ insecure_skip_tls_verify : false ,
419+ original_context_name : None ,
420+ } ;
421+ KubeManager :: add_token_cluster ( request) . expect ( "PEM CA should be accepted" ) ;
422+
423+ let kubeconfig = Kubeconfig :: read_from ( & path) . expect ( "kubeconfig should be readable" ) ;
424+ let stored_ca = kubeconfig
425+ . clusters
426+ . iter ( )
427+ . find ( |cluster| cluster. name == "pem-ca" )
428+ . and_then ( |cluster| cluster. cluster . as_ref ( ) )
429+ . and_then ( |cluster| cluster. certificate_authority_data . as_ref ( ) )
430+ . expect ( "CA data should be stored" ) ;
431+ let decoded = BASE64_STANDARD
432+ . decode ( stored_ca)
433+ . expect ( "stored CA should be valid base64" ) ;
434+
435+ assert_eq ! ( decoded, ca. trim( ) . as_bytes( ) ) ;
436+
437+ unsafe {
438+ std:: env:: remove_var ( "KUBECONFIG" ) ;
439+ }
440+ let _ = std:: fs:: remove_file ( & path) ;
441+ }
442+
343443 #[ test]
344444 fn add_token_cluster_reuses_token_for_non_conventional_user_name ( ) {
345445 let _guard = KUBECONFIG_ENV_LOCK . lock ( ) . unwrap ( ) ;
@@ -641,4 +741,20 @@ mod tests {
641741 . as_nanos( )
642742 ) )
643743 }
744+
745+ fn test_ca_pem ( ) -> String {
746+ String :: from (
747+ "-----BEGIN CERTIFICATE-----\n \
748+ MIIBszCCAVmgAwIBAgIUeH9mTSZQm2u9uU2xK4w6cmzNmjcwCgYIKoZIzj0EAwIw\n \
749+ FzEVMBMGA1UEAwwMYWV0aGVyaXMtdGVzdDAeFw0yNjAxMDEwMDAwMDBaFw0yNzAx\n \
750+ MDEwMDAwMDBaMBcxFTATBgNVBAMMDGFldGhlcmlzLXRlc3QwWTATBgcqhkjOPQIB\n \
751+ BggqhkjOPQMBBwNCAAQtsnR+S4p0lDNoe0JvLqR0ZGFxiiq7mU0u5KFLMZzU2l+O\n \
752+ fZgLr9LkqJYTVX9BYRZL2uY5pKiBmqnH6r8jo1MwUTAdBgNVHQ4EFgQUX0T5hZlP\n \
753+ Q0GYovLUG6CmH0iP2JwwHwYDVR0jBBgwFoAUX0T5hZlPQ0GYovLUG6CmH0iP2Jww\n \
754+ DwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgNIADBFAiEA7Lc8EX0GkL+5TMWX\n \
755+ B4JrLuN94ZG6wGB5SYrYd7Lj5P8CIBtMn+5Y0rQkgC0yQZdTx95k7iU01AOhKy4s\n \
756+ Qd6UpELK\n \
757+ -----END CERTIFICATE-----\n ",
758+ )
759+ }
644760}
0 commit comments