mirror of
https://github.com/lukaszraczylo/kubemirror.git
synced 2026-06-05 22:43:51 +00:00
fix: Mirrored resources managed by other operators.
This commit is contained in:
@@ -249,6 +249,137 @@ data:
|
||||
config: "value"
|
||||
```
|
||||
|
||||
## ExternalSecrets Integration
|
||||
|
||||
KubeMirror integrates seamlessly with the [ExternalSecrets Operator](https://external-secrets.io/) to distribute secrets from external stores (1Password, Vault, AWS Secrets Manager, etc.) across multiple namespaces.
|
||||
|
||||
### Overview
|
||||
|
||||
The `externalsecret-dockerconfig.yaml` example demonstrates how to:
|
||||
1. Sync secrets from 1Password/Vault/etc using ExternalSecrets
|
||||
2. Mirror those secrets to multiple namespaces using KubeMirror
|
||||
3. Avoid race conditions between the two controllers
|
||||
|
||||
### Prerequisites
|
||||
|
||||
```bash
|
||||
# Install ExternalSecrets Operator
|
||||
helm repo add external-secrets https://charts.external-secrets.io
|
||||
helm install external-secrets external-secrets/external-secrets -n external-secrets-system --create-namespace
|
||||
|
||||
# Configure your ClusterSecretStore (example for 1Password)
|
||||
kubectl apply -f your-clustersecretstore.yaml
|
||||
```
|
||||
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
# Apply the ExternalSecret example
|
||||
kubectl apply -f examples/externalsecret-dockerconfig.yaml
|
||||
|
||||
# Verify the source secret was created
|
||||
kubectl get secret multi-registry-secret -n default
|
||||
|
||||
# Verify mirrors were created by KubeMirror
|
||||
kubectl get secrets --all-namespaces -l kubemirror.raczylo.com/mirror=true
|
||||
|
||||
# Check sync status
|
||||
kubectl get secret multi-registry-secret -n default \
|
||||
-o jsonpath='{.metadata.annotations.kubemirror\.raczylo\.com/sync-status}'
|
||||
```
|
||||
|
||||
### Key Configuration
|
||||
|
||||
**Ownership Model**
|
||||
|
||||
KubeMirror uses **labels and annotations** to manage mirrors, not ownerReferences. This allows it to work with sources managed by any controller (ExternalSecrets, ArgoCD, etc.):
|
||||
|
||||
```yaml
|
||||
target:
|
||||
creationPolicy: Owner # Source can be owned by ExternalSecrets
|
||||
name: multi-registry-secret
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
kubemirror.raczylo.com/enabled: "true" # KubeMirror detection
|
||||
annotations:
|
||||
kubemirror.raczylo.com/sync: "true"
|
||||
kubemirror.raczylo.com/target-namespaces: "all"
|
||||
```
|
||||
|
||||
**Separation of Concerns:**
|
||||
|
||||
- **Source Secret**: Owned by ExternalSecrets (or any other controller) via `ownerReferences`
|
||||
- **Mirror Secrets**: Managed by KubeMirror via labels + annotations (no ownerReferences copied)
|
||||
- **Relationship**: Mirrors link to source via annotations (`source-namespace`, `source-name`, `source-uid`)
|
||||
- **Result**: Each controller manages its own resources independently
|
||||
|
||||
### Examples in externalsecret-dockerconfig.yaml
|
||||
|
||||
The example file contains three complete examples:
|
||||
|
||||
1. **Docker Registry Credentials** - Mirrors Docker config to all namespaces using `target-namespaces: "all"`
|
||||
2. **Database Credentials** - Uses `all-labeled` for opt-in mirroring with namespace labels
|
||||
3. **Namespace with Opt-In Label** - Shows how to label namespaces to receive mirrors
|
||||
|
||||
### Verification
|
||||
|
||||
```bash
|
||||
# Check ExternalSecret status
|
||||
kubectl get externalsecret 1p-docker-config -n default
|
||||
|
||||
# View the created secret
|
||||
kubectl get secret multi-registry-secret -n default -o yaml
|
||||
|
||||
# Verify KubeMirror picked it up
|
||||
kubectl logs -n kubemirror-system -l app.kubernetes.io/name=kubemirror | grep multi-registry-secret
|
||||
|
||||
# Count how many namespaces received the mirror
|
||||
kubectl get secrets --all-namespaces -l kubemirror.raczylo.com/mirror=true \
|
||||
--field-selector metadata.name=multi-registry-secret | wc -l
|
||||
|
||||
# Check a specific mirror
|
||||
kubectl get secret multi-registry-secret -n production-app -o yaml
|
||||
```
|
||||
|
||||
### Testing Updates
|
||||
|
||||
Test that ExternalSecrets refreshes propagate to all mirrors:
|
||||
|
||||
```bash
|
||||
# Force ExternalSecret refresh
|
||||
kubectl annotate externalsecret 1p-docker-config -n default \
|
||||
force-sync="$(date +%s)" --overwrite
|
||||
|
||||
# Wait for ExternalSecret to sync (check status)
|
||||
kubectl get externalsecret 1p-docker-config -n default -w
|
||||
|
||||
# Verify mirrors are updated (check generation or content hash)
|
||||
kubectl get secret multi-registry-secret -n production-app \
|
||||
-o jsonpath='{.metadata.annotations.kubemirror\.raczylo\.com/source-content-hash}'
|
||||
```
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Mirrors Not Created**
|
||||
- Verify the secret has `kubemirror.raczylo.com/enabled: "true"` label
|
||||
- Check that KubeMirror annotations are in the ExternalSecret template
|
||||
- View controller logs: `kubectl logs -n kubemirror-system -l app.kubernetes.io/name=kubemirror`
|
||||
|
||||
2. **ExternalSecret Not Syncing**
|
||||
- Check ExternalSecret status: `kubectl describe externalsecret <name> -n <namespace>`
|
||||
- Verify ClusterSecretStore is configured: `kubectl get clustersecretstore`
|
||||
- Check external-secrets-operator logs
|
||||
|
||||
### Alternative Backends
|
||||
|
||||
The example includes commented configurations for:
|
||||
- AWS Secrets Manager
|
||||
- HashiCorp Vault
|
||||
- Google Secret Manager
|
||||
|
||||
Uncomment and configure the ClusterSecretStore for your backend.
|
||||
|
||||
## Transformation Rules
|
||||
|
||||
KubeMirror supports transformation rules that modify resources during mirroring. This enables environment-specific configurations, security hardening, and dynamic value generation.
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
# Example: Using KubeMirror with ExternalSecrets Operator
|
||||
#
|
||||
# This example demonstrates how to use KubeMirror to distribute secrets from
|
||||
# external secret stores (1Password, Vault, AWS Secrets Manager, etc.) across
|
||||
# multiple namespaces.
|
||||
#
|
||||
# KubeMirror automatically strips ownerReferences from mirrors, so you can use
|
||||
# the standard ExternalSecrets creationPolicy: Owner without conflicts.
|
||||
#
|
||||
# Prerequisites:
|
||||
# 1. ExternalSecrets Operator installed: https://external-secrets.io/
|
||||
# 2. ClusterSecretStore configured for your secret backend
|
||||
# 3. KubeMirror installed and running
|
||||
#
|
||||
# Apply this manifest:
|
||||
# kubectl apply -f externalsecret-dockerconfig.yaml
|
||||
#
|
||||
# Verify:
|
||||
# kubectl get externalsecret 1p-docker-config -n default
|
||||
# kubectl get secret multi-registry-secret -n default
|
||||
# kubectl get secrets --all-namespaces -l kubemirror.raczylo.com/mirror=true
|
||||
|
||||
---
|
||||
apiVersion: external-secrets.io/v1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
name: 1p-docker-config
|
||||
namespace: default
|
||||
annotations:
|
||||
description: "Pulls Docker registry credentials from 1Password and mirrors to all namespaces"
|
||||
spec:
|
||||
# Secret backend configuration
|
||||
secretStoreRef:
|
||||
kind: ClusterSecretStore
|
||||
name: 1password-homecluster # Replace with your ClusterSecretStore name
|
||||
|
||||
# Refresh interval - how often to sync from external store
|
||||
refreshInterval: 24h
|
||||
|
||||
# Target secret configuration
|
||||
target:
|
||||
# Standard ExternalSecrets setting - KubeMirror automatically strips ownerReferences from mirrors
|
||||
creationPolicy: Owner
|
||||
|
||||
# Deletion policy - what happens when ExternalSecret is deleted
|
||||
# - Retain: Keep the secret (recommended with KubeMirror)
|
||||
# - Delete: Remove the secret
|
||||
deletionPolicy: Retain
|
||||
|
||||
# Name of the Kubernetes secret to create
|
||||
name: multi-registry-secret
|
||||
|
||||
# Template for the secret - includes KubeMirror annotations
|
||||
template:
|
||||
type: kubernetes.io/dockerconfigjson
|
||||
|
||||
# Metadata to include in the created secret
|
||||
metadata:
|
||||
labels:
|
||||
# REQUIRED: Server-side filtering label for KubeMirror
|
||||
kubemirror.raczylo.com/enabled: "true"
|
||||
|
||||
# Optional: Additional labels
|
||||
app: registry-credentials
|
||||
managed-by: external-secrets
|
||||
|
||||
annotations:
|
||||
# REQUIRED: Enable mirroring
|
||||
kubemirror.raczylo.com/sync: "true"
|
||||
|
||||
# Target namespaces - choose one of:
|
||||
# - Specific namespaces: "app1,app2,app3"
|
||||
# - Pattern matching: "app-*,prod-*"
|
||||
# - All namespaces: "all"
|
||||
# - Labeled namespaces only: "all-labeled"
|
||||
kubemirror.raczylo.com/target-namespaces: "all"
|
||||
|
||||
# Optional: Add description
|
||||
description: "Docker registry credentials synced from 1Password"
|
||||
|
||||
# Docker config JSON template using ExternalSecrets templating
|
||||
data:
|
||||
.dockerconfigjson: |
|
||||
{
|
||||
"auths": {
|
||||
"ghcr.io": {
|
||||
"username": "{{ .ghcrUsername | toString }}",
|
||||
"auth": "{{ printf "%s:%s" .ghcrUsername .ghcrPassword | b64enc }}"
|
||||
},
|
||||
"https://index.docker.io/v1/": {
|
||||
"username": "{{ .dockerUsername | toString }}",
|
||||
"auth": "{{ printf "%s:%s" .dockerUsername .dockerPassword | b64enc }}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Data mappings - what to fetch from external secret store
|
||||
data:
|
||||
# GitHub Container Registry credentials
|
||||
- remoteRef:
|
||||
key: DockerAuth/ghcrio_username
|
||||
property: username # Optional: if secret has multiple properties
|
||||
secretKey: ghcrUsername
|
||||
|
||||
- remoteRef:
|
||||
key: DockerAuth/ghcrio_password
|
||||
secretKey: ghcrPassword
|
||||
|
||||
# Docker Hub credentials
|
||||
- remoteRef:
|
||||
key: DockerAuth/dockerio_username
|
||||
secretKey: dockerUsername
|
||||
|
||||
- remoteRef:
|
||||
key: DockerAuth/dockerio_password
|
||||
secretKey: dockerPassword
|
||||
|
||||
---
|
||||
# Example: Using with all-labeled for opt-in mirroring
|
||||
apiVersion: external-secrets.io/v1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
name: 1p-database-credentials
|
||||
namespace: shared-resources
|
||||
spec:
|
||||
secretStoreRef:
|
||||
kind: ClusterSecretStore
|
||||
name: 1password-homecluster
|
||||
|
||||
refreshInterval: 24h
|
||||
|
||||
target:
|
||||
creationPolicy: Owner # Standard setting - KubeMirror strips ownerReferences
|
||||
deletionPolicy: Retain
|
||||
name: postgres-credentials
|
||||
|
||||
template:
|
||||
type: Opaque
|
||||
metadata:
|
||||
labels:
|
||||
kubemirror.raczylo.com/enabled: "true"
|
||||
annotations:
|
||||
kubemirror.raczylo.com/sync: "true"
|
||||
# Only mirror to namespaces with allow-mirrors label
|
||||
kubemirror.raczylo.com/target-namespaces: "all-labeled"
|
||||
|
||||
stringData:
|
||||
# ExternalSecrets templating for connection strings
|
||||
DATABASE_URL: "postgres://{{ .username }}:{{ .password }}@postgres.shared-resources.svc:5432/mydb"
|
||||
DB_USER: "{{ .username }}"
|
||||
DB_PASSWORD: "{{ .password }}"
|
||||
|
||||
data:
|
||||
- remoteRef:
|
||||
key: Database/postgres_username
|
||||
secretKey: username
|
||||
|
||||
- remoteRef:
|
||||
key: Database/postgres_password
|
||||
secretKey: password
|
||||
|
||||
---
|
||||
# Example namespace with allow-mirrors label (for all-labeled targeting)
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: production-app
|
||||
labels:
|
||||
# Opt-in to receive mirrored secrets
|
||||
kubemirror.raczylo.com/allow-mirrors: "true"
|
||||
environment: production
|
||||
|
||||
---
|
||||
# Alternative ClusterSecretStore examples for different backends
|
||||
# Uncomment and configure based on your secret backend
|
||||
|
||||
# # AWS Secrets Manager
|
||||
# apiVersion: external-secrets.io/v1beta1
|
||||
# kind: ClusterSecretStore
|
||||
# metadata:
|
||||
# name: aws-secrets-manager
|
||||
# spec:
|
||||
# provider:
|
||||
# aws:
|
||||
# service: SecretsManager
|
||||
# region: us-west-2
|
||||
# auth:
|
||||
# jwt:
|
||||
# serviceAccountRef:
|
||||
# name: external-secrets
|
||||
# namespace: external-secrets
|
||||
|
||||
# # HashiCorp Vault
|
||||
# apiVersion: external-secrets.io/v1beta1
|
||||
# kind: ClusterSecretStore
|
||||
# metadata:
|
||||
# name: vault-backend
|
||||
# spec:
|
||||
# provider:
|
||||
# vault:
|
||||
# server: "https://vault.example.com"
|
||||
# path: "secret"
|
||||
# version: "v2"
|
||||
# auth:
|
||||
# kubernetes:
|
||||
# mountPath: "kubernetes"
|
||||
# role: "external-secrets"
|
||||
# serviceAccountRef:
|
||||
# name: external-secrets
|
||||
# namespace: external-secrets
|
||||
|
||||
# # Google Secret Manager
|
||||
# apiVersion: external-secrets.io/v1beta1
|
||||
# kind: ClusterSecretStore
|
||||
# metadata:
|
||||
# name: google-secret-manager
|
||||
# spec:
|
||||
# provider:
|
||||
# gcpsm:
|
||||
# projectID: "my-project-id"
|
||||
# auth:
|
||||
# workloadIdentity:
|
||||
# clusterLocation: us-central1
|
||||
# clusterName: my-cluster
|
||||
# serviceAccountRef:
|
||||
# name: external-secrets
|
||||
# namespace: external-secrets
|
||||
Reference in New Issue
Block a user