@@ -8,6 +8,7 @@ use stackable_operator::{
88 k8s_openapi:: {
99 ByteString ,
1010 api:: core:: v1:: { ConfigMap , Secret } ,
11+ apimachinery:: pkg:: apis:: meta:: v1:: OwnerReference ,
1112 } ,
1213 kube:: {
1314 Resource ,
@@ -220,6 +221,27 @@ pub enum Error {
220221 source : stackable_operator:: client:: Error ,
221222 secret : ObjectRef < Secret > ,
222223 } ,
224+
225+ #[ snafu( display(
226+ "failed to look up pre-existing target {object} before applying the TrustStore"
227+ ) ) ]
228+ GetExistingTarget {
229+ source : stackable_operator:: client:: Error ,
230+ object : ObjectRef < stackable_operator:: kube:: api:: DynamicObject > ,
231+ } ,
232+
233+ #[ snafu( display(
234+ "refusing to overwrite pre-existing {object} that is not owned by this TrustStore"
235+ ) ) ]
236+ RefuseToOverwriteForeignTarget {
237+ object : ObjectRef < stackable_operator:: kube:: api:: DynamicObject > ,
238+ } ,
239+
240+ #[ snafu( display( "TrustStore has no associated UID" ) ) ]
241+ NoTrustStoreUid ,
242+
243+ #[ snafu( display( "TrustStore has no associated name" ) ) ]
244+ NoTrustStoreName ,
223245}
224246
225247type Result < T , E = Error > = std:: result:: Result < T , E > ;
@@ -239,10 +261,28 @@ impl ReconcilerError for Error {
239261 Error :: BuildOwnerReference { .. } => None ,
240262 Error :: ApplyTrustStoreConfigMap { config_map, .. } => Some ( config_map. clone ( ) . erase ( ) ) ,
241263 Error :: ApplyTrustStoreSecret { secret, .. } => Some ( secret. clone ( ) . erase ( ) ) ,
264+ Error :: GetExistingTarget { object, .. } => Some ( object. clone ( ) ) ,
265+ Error :: RefuseToOverwriteForeignTarget { object } => Some ( object. clone ( ) ) ,
266+ Error :: NoTrustStoreUid => None ,
267+ Error :: NoTrustStoreName => None ,
242268 }
243269 }
244270}
245271
272+ /// Returns `true` if `existing_owners` contain a controller `OwnerReference` that points to the
273+ /// TrustStore with the given UID. Used to ensure we only overwrite output ConfigMaps and Secrets
274+ /// that we previously created ourselves; refusing otherwise prevents the TrustStore primitive
275+ /// from being abused to clobber foreign same-named objects (e.g. `kube-root-ca.crt`) via the
276+ /// operator's elevated cluster-wide write permissions.
277+ fn is_owned_by_truststore ( existing_owners : & [ OwnerReference ] , truststore_uid : & str ) -> bool {
278+ existing_owners. iter ( ) . any ( |owner| {
279+ owner. controller == Some ( true )
280+ && owner. kind == "TrustStore"
281+ && owner. api_version . starts_with ( "secrets.stackable.tech/" )
282+ && owner. uid == truststore_uid
283+ } )
284+ }
285+
246286struct Ctx {
247287 client : stackable_operator:: client:: Client ,
248288}
@@ -311,6 +351,18 @@ async fn reconcile(
311351 . context ( BuildOwnerReferenceSnafu ) ?
312352 . build ( ) ;
313353
354+ let truststore_name = truststore
355+ . metadata
356+ . name
357+ . as_deref ( )
358+ . context ( NoTrustStoreNameSnafu ) ?;
359+ let truststore_namespace = selector. namespace . as_str ( ) ;
360+ let truststore_uid = truststore
361+ . metadata
362+ . uid
363+ . as_deref ( )
364+ . context ( NoTrustStoreUidSnafu ) ?;
365+
314366 match truststore. spec . target_kind {
315367 v1alpha1:: TrustStoreOutputType :: ConfigMap => {
316368 let trust_cm = ConfigMap {
@@ -319,6 +371,22 @@ async fn reconcile(
319371 binary_data : Some ( binary_data) ,
320372 ..Default :: default ( )
321373 } ;
374+ let existing = ctx
375+ . client
376+ . get_opt :: < ConfigMap > ( truststore_name, truststore_namespace)
377+ . await
378+ . with_context ( |_| GetExistingTargetSnafu {
379+ object : ObjectRef :: from_obj ( & trust_cm) . erase ( ) ,
380+ } ) ?;
381+ if let Some ( existing) = existing {
382+ let owners = existing. metadata . owner_references . as_deref ( ) . unwrap_or ( & [ ] ) ;
383+ if !is_owned_by_truststore ( owners, truststore_uid) {
384+ return RefuseToOverwriteForeignTargetSnafu {
385+ object : ObjectRef :: from_obj ( & trust_cm) . erase ( ) ,
386+ }
387+ . fail ( ) ;
388+ }
389+ }
322390 ctx. client
323391 . apply_patch ( CONTROLLER_NAME , & trust_cm, & trust_cm)
324392 . await
@@ -333,6 +401,22 @@ async fn reconcile(
333401 data : Some ( binary_data) ,
334402 ..Default :: default ( )
335403 } ;
404+ let existing = ctx
405+ . client
406+ . get_opt :: < Secret > ( truststore_name, truststore_namespace)
407+ . await
408+ . with_context ( |_| GetExistingTargetSnafu {
409+ object : ObjectRef :: from_obj ( & trust_secret) . erase ( ) ,
410+ } ) ?;
411+ if let Some ( existing) = existing {
412+ let owners = existing. metadata . owner_references . as_deref ( ) . unwrap_or ( & [ ] ) ;
413+ if !is_owned_by_truststore ( owners, truststore_uid) {
414+ return RefuseToOverwriteForeignTargetSnafu {
415+ object : ObjectRef :: from_obj ( & trust_secret) . erase ( ) ,
416+ }
417+ . fail ( ) ;
418+ }
419+ }
336420 ctx. client
337421 . apply_patch ( CONTROLLER_NAME , & trust_secret, & trust_secret)
338422 . await
@@ -352,3 +436,84 @@ fn error_policy(
352436) -> controller:: Action {
353437 controller:: Action :: requeue ( Duration :: from_secs ( 5 ) )
354438}
439+
440+ #[ cfg( test) ]
441+ mod tests {
442+ use super :: * ;
443+
444+ fn owner_ref (
445+ api_version : & str ,
446+ kind : & str ,
447+ uid : & str ,
448+ controller : Option < bool > ,
449+ ) -> OwnerReference {
450+ OwnerReference {
451+ api_version : api_version. to_string ( ) ,
452+ kind : kind. to_string ( ) ,
453+ name : "some-name" . to_string ( ) ,
454+ uid : uid. to_string ( ) ,
455+ controller,
456+ block_owner_deletion : None ,
457+ }
458+ }
459+
460+ #[ test]
461+ fn empty_owner_refs_are_rejected ( ) {
462+ assert ! ( !is_owned_by_truststore( & [ ] , "my-uid" ) ) ;
463+ }
464+
465+ #[ test]
466+ fn matching_controller_owner_ref_is_accepted ( ) {
467+ let owners = [ owner_ref (
468+ "secrets.stackable.tech/v1alpha1" ,
469+ "TrustStore" ,
470+ "my-uid" ,
471+ Some ( true ) ,
472+ ) ] ;
473+ assert ! ( is_owned_by_truststore( & owners, "my-uid" ) ) ;
474+ }
475+
476+ #[ test]
477+ fn non_controller_owner_ref_is_rejected ( ) {
478+ let owners = [ owner_ref (
479+ "secrets.stackable.tech/v1alpha1" ,
480+ "TrustStore" ,
481+ "my-uid" ,
482+ Some ( false ) ,
483+ ) ] ;
484+ assert ! ( !is_owned_by_truststore( & owners, "my-uid" ) ) ;
485+ }
486+
487+ #[ test]
488+ fn different_uid_is_rejected ( ) {
489+ let owners = [ owner_ref (
490+ "secrets.stackable.tech/v1alpha1" ,
491+ "TrustStore" ,
492+ "other-uid" ,
493+ Some ( true ) ,
494+ ) ] ;
495+ assert ! ( !is_owned_by_truststore( & owners, "my-uid" ) ) ;
496+ }
497+
498+ #[ test]
499+ fn different_kind_is_rejected ( ) {
500+ let owners = [ owner_ref ( "v1" , "ConfigMap" , "my-uid" , Some ( true ) ) ] ;
501+ assert ! ( !is_owned_by_truststore( & owners, "my-uid" ) ) ;
502+ }
503+
504+ #[ test]
505+ fn foreign_api_group_is_rejected ( ) {
506+ let owners = [ owner_ref (
507+ "evil.example.com/v1" ,
508+ "TrustStore" ,
509+ "my-uid" ,
510+ Some ( true ) ,
511+ ) ] ;
512+ assert ! ( !is_owned_by_truststore( & owners, "my-uid" ) ) ;
513+ }
514+
515+ #[ test]
516+ fn kube_root_ca_style_no_owner_is_rejected ( ) {
517+ assert ! ( !is_owned_by_truststore( & [ ] , "my-uid" ) ) ;
518+ }
519+ }
0 commit comments