Kubernetes operator that automatically syncs Secrets & ConfigMaps across namespaces — glob filtering, auto-detection, and Prometheus metrics.
K8s Namespace Sync is a Kubernetes controller that automatically synchronizes Secrets and ConfigMaps across multiple namespaces within a Kubernetes cluster.
- Automatic synchronization of Secrets and ConfigMaps across namespaces
- Automatic detection and synchronization of changes in source namespace
- Support for selective namespace targeting
- Automatic exclusion of system namespaces (kube-system, kube-public, etc.)
- Support for manually excluding specific namespaces
- Prometheus metrics support
- Synchronization status monitoring
- Resource filtering with glob patterns that narrow the
secretName/configMapNamelists
The controller:
- Watches for changes in the source namespace's Secrets and ConfigMaps
- Automatically syncs changes to all target namespaces
- Maintains consistency by cleaning up resources when source is deleted
- Deletes copies the spec no longer asks for (a name dropped from the list, a namespace newly excluded or left out of
targetNamespaces, a tightened filter), unless another NamespaceSync still syncs them - Uses finalizers to ensure proper cleanup during deletion
- Kubernetes v1.25+ (the CRD ships CEL validation rules, which require
x-kubernetes-validations) - kubectl v1.11.3+
Recommended: OCI registry (Helm 3.8+)
# Single-command install — no helm repo add needed
helm install k8s-namespace-sync oci://ghcr.io/somaz94/charts/k8s-namespace-sync \
--version 0.6.1Alternative: classic Helm repo
# Add the Helm repository
helm repo add k8s-namespace-sync https://somaz94.github.io/k8s-namespace-sync/helm-repo
helm repo update
# Install with default values
helm install k8s-namespace-sync k8s-namespace-sync/k8s-namespace-sync
# Or install with custom values
helm install k8s-namespace-sync k8s-namespace-sync/k8s-namespace-sync \
--set image.tag=v0.6.1The chart creates the namespace named by its namespace value (k8s-namespace-sync-system unless you change it), so do not add --namespace k8s-namespace-sync-system --create-namespace: Helm would create that namespace first, and the install then fails with namespaces "k8s-namespace-sync-system" already exists.
For full Helm chart options, see Helm README.
kubectl apply -f https://raw.githubusercontent.com/somaz94/k8s-namespace-sync/main/dist/install.yaml# Clone the repository
git clone https://github.com/somaz94/k8s-namespace-sync.git
cd k8s-namespace-sync
# Install CRDs
make install
# Deploy the controller
make deploy IMG=somaz940/k8s-namespace-sync:v0.6.1apiVersion: v1
kind: Secret
metadata:
name: test-secret
namespace: default
type: Opaque
stringData:
username: admin
password: secret123
---
apiVersion: v1
kind: ConfigMap
metadata:
name: test-configmap
namespace: default
data:
key1: value1
key2: value2if you want to test with multiple resources, you can apply the following resources:
kubectl apply -f https://raw.githubusercontent.com/somaz94/k8s-namespace-sync/main/release/examples/test-configmap-secret/test-configmap.yaml
kubectl apply -f https://raw.githubusercontent.com/somaz94/k8s-namespace-sync/main/release/examples/test-configmap-secret/test-configmap2.yaml
kubectl apply -f https://raw.githubusercontent.com/somaz94/k8s-namespace-sync/main/release/examples/test-configmap-secret/test-secret.yaml
kubectl apply -f https://raw.githubusercontent.com/somaz94/k8s-namespace-sync/main/release/examples/test-configmap-secret/test-secret2.yaml- Important Note: Each CR should be tested individually to avoid conflicts. After testing each CR, make sure to delete it before testing the next one.
Basic synchronization (sync to all namespaces):
apiVersion: sync.nsync.dev/v1
kind: NamespaceSync
metadata:
name: namespacesync-sample
finalizers:
- namespacesync.nsync.dev/finalizer
spec:
sourceNamespace: default
configMapName:
- test-configmap
secretName:
- test-secretBasic apply and test:
kubectl apply -f https://raw.githubusercontent.com/somaz94/k8s-namespace-sync/main/release/examples/sync_v1_namespacesync.yaml
# Check the resources in the target namespaces
k get namespacesyncs.sync.nsync.dev
# Delete the CR after testing
kubectl delete -f https://raw.githubusercontent.com/somaz94/k8s-namespace-sync/main/release/examples/sync_v1_namespacesync.yamlWith specific target namespaces:
apiVersion: sync.nsync.dev/v1
kind: NamespaceSync
metadata:
name: namespacesync-sample-targets
finalizers:
- namespacesync.nsync.dev/finalizer
spec:
sourceNamespace: default
targetNamespaces: # Only sync to these namespaces
- test-ns1
- test-ns2
configMapName:
- test-configmap
secretName:
- test-secretTarget apply and test:
kubectl apply -f https://raw.githubusercontent.com/somaz94/k8s-namespace-sync/main/release/examples/sync_v1_namespacesync_target.yaml
# Check the resources in the target namespaces
kubectl get namespacesyncs.sync.nsync.dev
# Create target namespaces
kubectl create ns test-ns1
kubectl create ns test-ns2
# Check the resources in the target namespaces
kubectl get secret,configmap -n test-ns1
kubectl get secret,configmap -n test-ns2
# Check the resources in the another namespace
kubectl get secret,configmap -n <another-namespace>
# Delete the CR after testing
kubectl delete -f https://raw.githubusercontent.com/somaz94/k8s-namespace-sync/main/release/examples/sync_v1_namespacesync_target.yamlWith excluded namespaces:
apiVersion: sync.nsync.dev/v1
kind: NamespaceSync
metadata:
name: namespacesync-sample
finalizers:
- namespacesync.nsync.dev/finalizer
spec:
sourceNamespace: default
configMapName:
- test-configmap
- test-configmap2
secretName:
- test-secret
- test-secret2
exclude:
- test-ns2
- test-ns3
Exclude apply and test:
kubectl apply -f https://raw.githubusercontent.com/somaz94/k8s-namespace-sync/main/release/examples/sync_v1_namespacesync_exclude.yaml
# Check the resources in the target namespaces
k get namespacesyncs.sync.nsync.dev
# Create target namespaces
kubectl create ns test-ns2
kubectl create ns test-ns3
# Check the resources in the target namespaces
kubectl get secret,configmap -n test-ns2
kubectl get secret,configmap -n test-ns3
# Check the resources in the another namespace
kubectl get secret,configmap -n <another-namespace>
# Delete the CR after testing
kubectl delete -f https://raw.githubusercontent.com/somaz94/k8s-namespace-sync/main/release/examples/sync_v1_namespacesync_exclude.yamlWith resource filters:
apiVersion: sync.nsync.dev/v1
kind: NamespaceSync
metadata:
name: namespacesync-filter-sample
finalizers:
- namespacesync.nsync.dev/finalizer
spec:
sourceNamespace: default
configMapName:
- test-configmap
- test-configmap2
secretName:
- test-secret
- test-secret2
resourceFilters:
configMaps:
# include:
# - "test-configmap*" # Listed ConfigMaps starting with test-configmap
exclude:
- "*2" # Exclude ConfigMaps ending with 2
secrets:
# include:
# - "test-secret*" # Listed Secrets starting with test-secret
exclude:
- "*2" # Exclude Secrets ending with 2
exclude:
- test-ns2
- test-ns3Filter apply and test:
kubectl apply -f https://raw.githubusercontent.com/somaz94/k8s-namespace-sync/main/release/examples/sync_v1_namespacesync_filter.yaml
# Check the resources in the target namespaces
kubectl get namespacesyncs.sync.nsync.dev
# Create target namespaces
kubectl create ns test-ns2
kubectl create ns test-ns3
# Check the resources in the target namespaces
kubectl get secret,configmap -n test-ns2
kubectl get secret,configmap -n test-ns3
# Check the resources in the another namespace
# Check the resources in the filtered configmap
kubectl get secret,configmap -n <another-namespace>
# Delete the CR after testing
kubectl delete -f https://raw.githubusercontent.com/somaz94/k8s-namespace-sync/main/release/examples/sync_v1_namespacesync_filter.yaml- If
targetNamespacesis not specified, resources will be synced to all namespaces (except excluded ones) - If
targetNamespacesis specified, resources will only be synced to the listed namespaces - System namespaces and source namespace are always excluded
excludelist takes precedence overtargetNamespaces- Changes in source resources are automatically detected and synced in real-time
- Deleting a resource from the source namespace removes its synced copies from the target namespaces
- Labels and annotations from the source resources are preserved in synced resources, except keys under
kubernetes.io/andkubectl.kubernetes.io/(such askubectl.kubernetes.io/last-applied-configuration), which describe the source object itself - When the NamespaceSync CR is deleted, all synced resources are automatically cleaned up
- Deletion only touches copies the controller created, identified by the
namespacesync.nsync.dev/source-namespaceandnamespacesync.nsync.dev/source-nameannotations. A same-named object it never synced, such as one a resource filter excluded, is left in place - A same-named object without those annotations is overwritten only in a namespace listed in
targetNamespaces, and only if no other NamespaceSync reads it as its source; from then on it counts as a synced copy. In a namespace reached becausetargetNamespacesis empty, it is left alone and reported as a sync conflict - Deleting a NamespaceSync keeps a copy that another NamespaceSync still syncs from the same source into that namespace, and an object another NamespaceSync reads as its source
- NamespaceSyncs that sync the same name into the same namespace do not overwrite each other; see Overlapping NamespaceSyncs
spec.sourceNamespacecannot be changed after creation. To sync from another namespace, delete the NamespaceSync, whose finalizer removes the old copies, and create a new one- Finalizer ensures proper cleanup of synced resources before CR deletion
- The controller performs periodic reconciliation (default: every 5 minutes) to catch any external drift, ensuring resources remain in sync even if events are missed. The interval is configurable via the
RECONCILE_INTERVALenvironment variable (e.g.,RECONCILE_INTERVAL=10m)
When several NamespaceSyncs sync the same name into the same namespace:
- A namespace a NamespaceSync reaches only because its
targetNamespacesis empty never receives an object another NamespaceSync reads as its source, or a copy another NamespaceSync still syncs from a different source namespace - A namespace listed in
targetNamespacesis treated as intent. The NamespaceSync keeps its own copy there even when another NamespaceSync reads that copy as its source, so syncs can chain (atob, thenbtoc), and it takes over a copy from a NamespaceSync that reaches the namespace only by default - An object created by hand that another NamespaceSync reads as its source is never overwritten
- When two NamespaceSyncs both list the namespace, the copy written first stays
- A skipped object is listed in
status.failedNamespaceswith aSyncConflictevent, and the other resources in that namespace still sync. A copy whose source no NamespaceSync syncs any more is taken over - A NamespaceSync with an empty
targetNamespacesnever overwrites an object created by hand, whether or not another NamespaceSync uses it, so deleting and recreating a NamespaceSync does not expose its hand-made source. A copy is protected only while the NamespaceSync that syncs it exists - One NamespaceSync's spec can block another's writes, so give NamespaceSync edit rights only to people trusted with every namespace it can reach
The controller emits Kubernetes events on NamespaceSync resources to provide visibility into sync operations:
| Event Type | Reason | Description |
|---|---|---|
| Normal | SyncComplete |
Emitted after resources are successfully synced to target namespaces |
| Warning | SyncFailed |
Emitted when sync fails for one or more target namespaces for a reason other than a sync conflict |
| Warning | ValidationFailed |
Emitted when the NamespaceSync spec fails validation |
| Warning | SyncConflict |
Emitted when a sync skips an object that another NamespaceSync owns, or that someone created by hand in a namespace reached by default (see Overlapping NamespaceSyncs) |
| Normal | CleanupComplete |
Emitted after synced resources are successfully cleaned up during deletion |
| Warning | CleanupFailed |
Emitted when cleanup of synced resources fails during deletion |
You can view events with:
kubectl describe namespacesync <name>
# or
kubectl get events --field-selector involvedObject.kind=NamespaceSyncSynced resources are annotated with metadata that tracks their origin and sync status:
| Annotation | Description |
|---|---|
namespacesync.nsync.dev/source-namespace |
The namespace from which the resource was synced |
namespacesync.nsync.dev/source-name |
The name of the source resource |
namespacesync.nsync.dev/last-sync |
When the controller last wrote the copy's data (RFC3339 format); an update that changes only labels or annotations keeps it |
These annotations allow you to identify which resources are managed by NamespaceSync and trace them back to their source.
Copies keep exactly the labels and annotations of their source. When a mutating admission policy or webhook adds labels or annotations to the copies, a sync removes them and admission adds them back in the same request, so as long as admission writes the same values every time, the stored copy does not change and nothing is rewritten. Admission that changes the data or writes a different value each time, and another controller that edits the copies after they are written, still make every sync rewrite the copy; exclude the copies from those.
The controller sets a Ready condition on each NamespaceSync resource to reflect the current sync state:
| Reason | Status | Description |
|---|---|---|
SyncComplete |
True |
All target namespaces were synced successfully |
SyncConflict |
True |
Every namespace that did not sync only skipped objects another owner holds (see Overlapping NamespaceSyncs); nothing failed |
PartialSync |
True |
Some target namespaces were synced, but others failed for a reason other than a sync conflict |
SyncFailed |
False |
All target namespaces failed to sync |
InvalidSpec |
False |
The spec failed validation; the message holds the error |
You can inspect the condition with:
kubectl get namespacesync <name> -o jsonpath='{.status.conditions[?(@.type=="Ready")]}'The controller exposes the following Prometheus metrics:
| Metric | Type | Labels | Description |
|---|---|---|---|
namespacesync_sync_success_total |
Counter | namespace, resource_type |
Number of successful resource synchronizations |
namespacesync_sync_failure_total |
Counter | namespace, resource_type |
Number of failed resource synchronizations; sync conflicts are counted in namespacesync_sync_conflict_total instead |
namespacesync_sync_conflict_total |
Counter | namespace, resource_type |
Number of resource synchronizations skipped as a sync conflict (see Overlapping NamespaceSyncs) |
namespacesync_cleanup_success_total |
Counter | namespace, resource_type |
Number of synced copies deleted during cleanup |
namespacesync_cleanup_failure_total |
Counter | namespace, resource_type |
Number of failed resource cleanups |
namespacesync_sync_duration_seconds |
Histogram | namespace, resource_type |
Duration of sync operations in seconds |
namespacesync_managed_resources |
Gauge | namespace, resource_type |
Number of resources being managed by NamespaceSync |
- Supports both include and exclude patterns for ConfigMaps and Secrets
- Patterns narrow the names already listed in
secretName/configMapName; they do not select other objects from the source namespace - Uses glob pattern matching (e.g., "*" for any characters)
- Include patterns specify which resources to sync
- Exclude patterns specify which resources to skip
- If both include and exclude patterns are specified, exclude takes precedence
- Patterns are matched against resource names
- Can be combined with namespace targeting and exclusion
- A pattern that is not a valid glob (for example
[abc) fails validation: the controller emits aValidationFailedevent and sets the Ready condition toFalsewith reasonInvalidSpecinstead of syncing
kubectl get namespacesync namespacesync-sample -o yamlkubectl get secret test-secret -n target-namespace
kubectl get configmap test-configmap -n target-namespaceThe CRD enforces the following rules at admission time. They are part of the CRD schema (field constraints and CEL x-kubernetes-validations), so an invalid resource is rejected by kubectl apply rather than being accepted and then failing on every reconcile.
| Rule | Message |
|---|---|
spec.sourceNamespace must be a non-empty DNS-1123 label (1–63 chars) |
spec.sourceNamespace in body should match ... |
At least one of spec.secretName or spec.configMapName must be non-empty |
at least one secret or configmap must be specified |
spec.sourceNamespace cannot change after creation |
sourceNamespace is immutable; delete and recreate the NamespaceSync to change it |
The controller keeps the same checks, except immutability, as a reconcile-time backstop, so a resource created before these rules existed still reports the error in its status instead of syncing nothing silently.
The controller also rejects resource filter patterns that are not valid globs. That check has no admission-time equivalent, so such a resource is accepted by kubectl apply and then reports the error in its status.
Note that listing the source namespace in spec.targetNamespaces, or a namespace in both spec.targetNamespaces and spec.exclude, is not an error — the source namespace is always skipped and exclude is evaluated first.
The following namespaces are automatically excluded from synchronization:
- kube-system
- kube-public
- kube-node-lease
- default
- the namespace the controller runs in (
k8s-namespace-sync-systemunless installed elsewhere)
Additionally, you can manually exclude specific namespaces using the exclude field in the NamespaceSync CR.
Common issues and solutions:
-
Resources not syncing:
- Check if namespace is in exclude list
- Verify controller logs:
kubectl logs -n k8s-namespace-sync-system -l control-plane=controller-manager - Check NamespaceSync status:
kubectl get namespacesync <name> -o yaml
-
Permission issues:
- Ensure RBAC permissions are properly configured
- Check if ServiceAccount has necessary permissions
-
Cleanup issues:
- Ensure finalizer is present in CR
- Check controller logs for cleanup errors
- Delete the ConfigMap and Secret resources:
kubectl delete configmap test-configmap
kubectl delete secret test-secret
kubectl delete configmap test-configmap2
kubectl delete secret test-secret2- Remove the NamespaceSync CR:
kubectl delete namespacesync --all -A- Remove the controller:
kubectl delete -f https://raw.githubusercontent.com/somaz94/k8s-namespace-sync/main/dist/install.yamlAll required tools will be automatically downloaded to ./bin directory when running:
make install-toolsOr you can install individual tools:
# Install controller-gen
make controller-gen # v0.16.4
# Install kustomize
make kustomize # v5.5.0
# Install setup-envtest
make envtest # v0.19.0
# Install golangci-lint
make golangci-lint # v1.61.0Manual installation locations:
- All tools will be installed in
./bindirectory - Specific versions:
- controller-gen v0.16.4
- kustomize v5.5.0
- setup-envtest v0.19.0
- golangci-lint v2.1.6
Note: The binary directory (./bin) is git-ignored and will be created when needed.
| Document | Description |
|---|---|
| Helm Chart | Helm chart installation, configuration, and values reference |
| Troubleshooting | Common issues and solutions |
| Version Bump | Checklist for releasing a new version |
| Contributing | How to contribute to this project |
Issues and pull requests are welcome.
- helios-lb: A bare-metal LoadBalancer for Kubernetes, a lightweight MetalLB alternative.
- network-policy-generator: An operator that learns pod traffic and generates NetworkPolicies from it.
- kube-drift: An operator that runs the same manifest-vs-cluster comparison on a schedule, inside the cluster.
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
