fix(controller): guard mirror deletion + enforce secret blacklist

C1: deleteAllMirrors used to issue a blind Delete on every namespace
matching the source name+GVK, which would destroy unrelated resources
(e.g. a 'default' SA, 'ca-bundle' ConfigMap) sharing the source name.
Now reads each candidate, verifies managed-by label and source-reference
annotation, and only deletes confirmed mirrors.

M1: BlacklistedSecretTypes was declared but never enforced. Enabling
mirroring on a service-account-token / bootstrap-token / helm release
Secret would mirror credentials cluster-wide. Now refused at Reconcile.

M3: deleteAllMirrors swallowed per-namespace errors and returned nil,
so callers removed the finalizer even on partial failure (orphans).
Errors are now joined and returned.
This commit is contained in:
2026-05-02 22:35:40 +01:00
parent b555d84d32
commit 4277c8ac39
2 changed files with 292 additions and 17 deletions
+68 -17
View File
@@ -3,6 +3,7 @@ package controller
import (
"context"
stderrors "errors"
"fmt"
"slices"
"time"
@@ -197,6 +198,22 @@ func (r *SourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
return ctrl.Result{}, nil
}
// Refuse to mirror sensitive Secret types (service-account tokens, bootstrap
// tokens, helm release blobs). Mirroring these to other namespaces is a
// credential exposure path. If a finalizer is already set (the resource was
// enabled before we started enforcing this list), tear down via handleDisabled
// so any prior mirrors are cleaned up.
if isBlacklistedSecret(sourceObj) {
secretType, _, _ := unstructured.NestedString(sourceObj.Object, "type")
logger.Info("refusing to mirror blacklisted Secret type",
"type", secretType,
"reason", "credential exposure risk")
if slices.Contains(sourceObj.GetFinalizers(), constants.FinalizerName) {
return r.handleDisabled(ctx, sourceObj)
}
return ctrl.Result{}, nil
}
// Add finalizer if not present
if !slices.Contains(sourceObj.GetFinalizers(), constants.FinalizerName) {
logger.Info("adding finalizer to source resource")
@@ -401,44 +418,65 @@ func (r *SourceReconciler) reconcileMirror(ctx context.Context, source runtime.O
return nil
}
// deleteAllMirrors deletes all mirrors for a source resource.
// deleteAllMirrors deletes all mirrors that this source owns across the cluster.
// It verifies ownership (managed-by label + source-reference annotation) before
// deleting anything to avoid destroying unrelated resources that happen to share
// the source's name. Per-namespace failures are aggregated so callers can defer
// finalizer removal until cleanup actually succeeds.
func (r *SourceReconciler) deleteAllMirrors(ctx context.Context, sourceObj metav1.Object) error {
logger := log.FromContext(ctx)
// List all namespaces
allNamespaces, err := r.NamespaceLister.ListNamespaces(ctx)
if err != nil {
return fmt.Errorf("failed to list namespaces: %w", err)
}
// Get GVK from source object
sourceUnstructured, ok := sourceObj.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("source object is not unstructured")
}
var deleteCount int
var (
deleteCount int
deleteErrs []error
)
for _, ns := range allNamespaces {
// Skip source namespace
if ns == sourceObj.GetNamespace() {
continue
}
// Create mirror reference for deletion
mirror := &unstructured.Unstructured{}
mirror.SetGroupVersionKind(sourceUnstructured.GroupVersionKind())
mirror.SetNamespace(ns)
mirror.SetName(sourceObj.GetName())
err := r.Delete(ctx, mirror)
if err == nil {
deleteCount++
} else if !errors.IsNotFound(err) {
logger.Error(err, "failed to delete mirror", "namespace", ns)
existing := &unstructured.Unstructured{}
existing.SetGroupVersionKind(sourceUnstructured.GroupVersionKind())
getErr := r.Get(ctx, client.ObjectKey{Namespace: ns, Name: sourceObj.GetName()}, existing)
if errors.IsNotFound(getErr) {
continue
}
if getErr != nil {
logger.Error(getErr, "failed to fetch potential mirror", "namespace", ns)
deleteErrs = append(deleteErrs, fmt.Errorf("get mirror %s/%s: %w", ns, sourceObj.GetName(), getErr))
continue
}
if !IsManagedByUs(existing) {
continue
}
srcNs, srcName, _, found := GetSourceReference(existing)
if !found || srcNs != sourceObj.GetNamespace() || srcName != sourceObj.GetName() {
continue
}
if delErr := r.Delete(ctx, existing); delErr != nil && !errors.IsNotFound(delErr) {
logger.Error(delErr, "failed to delete mirror", "namespace", ns)
deleteErrs = append(deleteErrs, fmt.Errorf("delete mirror %s/%s: %w", ns, sourceObj.GetName(), delErr))
continue
}
deleteCount++
}
logger.Info("deleted mirrors", "count", deleteCount)
logger.Info("deleted mirrors", "count", deleteCount, "errors", len(deleteErrs))
if len(deleteErrs) > 0 {
return stderrors.Join(deleteErrs...)
}
return nil
}
@@ -603,6 +641,19 @@ func (r *SourceReconciler) updateLastSyncStatus(ctx context.Context, source runt
return r.Update(ctx, source.(*unstructured.Unstructured))
}
// isBlacklistedSecret reports whether the given source is a core/v1 Secret with
// a Type that must never be mirrored across namespaces.
func isBlacklistedSecret(obj *unstructured.Unstructured) bool {
if obj.GetKind() != "Secret" || obj.GetAPIVersion() != "v1" {
return false
}
secretType, found, err := unstructured.NestedString(obj.Object, "type")
if err != nil || !found {
return false
}
return slices.Contains(constants.BlacklistedSecretTypes, secretType)
}
// isEnabledForMirroring checks if a resource has both the label and annotation for mirroring.
func isEnabledForMirroring(obj metav1.Object) bool {
// Check label