This guide shows how to deploy the vault-sync-operator across multiple clusters using the standard per-cluster deployment pattern.
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Cluster A │ │ Cluster B │ │ Cluster C │
│ │ │ │ │ │
│ ┌─────────────┐ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │
│ │ Operator │ │ │ │ Operator │ │ │ │ Operator │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ └─────────────┘ │ │ └─────────────┘ │ │ └─────────────┘ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
│ │ │
└───────────────────────┼───────────────────────┘
│
┌─────────────────┐
│ Shared Vault │
│ Server │
└─────────────────┘
Configure Vault with separate auth backends for each cluster:
# Enable Kubernetes auth for each cluster
vault auth enable -path=kubernetes-cluster-a kubernetes
vault auth enable -path=kubernetes-cluster-b kubernetes
vault auth enable -path=kubernetes-cluster-c kubernetes
# Configure each auth backend with cluster-specific settings
vault write auth/kubernetes-cluster-a/config \
token_reviewer_jwt="$(cat /cluster-a-sa-token)" \
kubernetes_host="https://cluster-a-api:443" \
kubernetes_ca_cert=@/cluster-a-ca.crt
vault write auth/kubernetes-cluster-b/config \
token_reviewer_jwt="$(cat /cluster-b-sa-token)" \
kubernetes_host="https://cluster-b-api:443" \
kubernetes_ca_cert=@/cluster-b-ca.crt
# Create cluster-specific policies
vault policy write vault-sync-cluster-a - <<EOF
path "clusters/cluster-a/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
EOF
vault policy write vault-sync-cluster-b - <<EOF
path "clusters/cluster-b/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
EOF
# Create roles for each cluster
vault write auth/kubernetes-cluster-a/role/vault-sync-operator \
bound_service_account_names=vault-sync-operator-controller-manager \
bound_service_account_namespaces=vault-sync-operator-system \
policies=vault-sync-cluster-a \
ttl=24h
vault write auth/kubernetes-cluster-b/role/vault-sync-operator \
bound_service_account_names=vault-sync-operator-controller-manager \
bound_service_account_namespaces=vault-sync-operator-system \
policies=vault-sync-cluster-b \
ttl=24h
# cluster-a-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: vault-sync-operator-controller-manager
namespace: vault-sync-operator-system
spec:
replicas: 1
selector:
matchLabels:
control-plane: controller-manager
template:
metadata:
labels:
control-plane: controller-manager
spec:
containers:
- name: manager
image: vault-sync-operator:latest
args:
- --leader-elect
- --vault-addr=$(VAULT_ADDR)
- --vault-role=vault-sync-operator
- --vault-auth-path=kubernetes-cluster-a # Cluster-specific auth path
- --cluster-name=cluster-a # Cluster identifier
env:
- name: VAULT_ADDR
value: "https://vault.company.com:8200"
- name: GOMEMLIMIT
valueFrom:
resourceFieldRef:
resource: limits.memory
resources:
limits:
cpu: 500m
memory: 128Mi
requests:
cpu: 10m
memory: 64Mi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
livenessProbe:
httpGet:
path: /healthz
port: 8081
readinessProbe:
httpGet:
path: /readyz
port: 8081
serviceAccountName: vault-sync-operator-controller-manager
securityContext:
runAsNonRoot: true
# cluster-b-deployment.yaml
# Same as cluster-a but with different values:
args:
- --vault-auth-path=kubernetes-cluster-b
- --cluster-name=cluster-b
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: default
annotations:
vault-sync.io/path: "secret/data/my-app"
# Will be stored at: clusters/cluster-a/secret/data/my-app
spec:
# ... deployment spec
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: default
annotations:
vault-sync.io/path: "secret/data/my-app"
# Will be stored at: clusters/cluster-b/secret/data/my-app
spec:
# ... deployment spec
With cluster-aware paths, secrets are organized as:
vault/
├── clusters/
│ ├── cluster-a/
│ │ ├── secret/data/my-app
│ │ ├── secret/data/database
│ │ └── secret/data/api-keys
│ ├── cluster-b/
│ │ ├── secret/data/my-app
│ │ ├── secret/data/frontend
│ │ └── secret/data/backend
│ └── cluster-c/
│ ├── secret/data/analytics
│ └── secret/data/monitoring
This prevents path conflicts between clusters while maintaining clear organization.
Each operator instance exposes its own metrics endpoint. Use Prometheus federation or a centralized monitoring solution:
# prometheus-config.yaml
scrape_configs:
- job_name: 'vault-sync-cluster-a'
kubernetes_sd_configs:
- role: pod
kubeconfig_file: /cluster-a-kubeconfig
relabel_configs:
- source_labels: [__meta_kubernetes_pod_label_control_plane]
action: keep
regex: controller-manager
- source_labels: [__meta_kubernetes_pod_container_port_name]
action: keep
regex: metrics
- job_name: 'vault-sync-cluster-b'
kubernetes_sd_configs:
- role: pod
kubeconfig_file: /cluster-b-kubeconfig
# ... similar config
--vault-auth-path matches the Vault auth backend name# Check operator logs in each cluster
kubectl logs -n vault-sync-operator-system deployment/vault-sync-operator-controller-manager
# Verify Vault paths from each cluster
vault kv list clusters/cluster-a/secret/data/
vault kv list clusters/cluster-b/secret/data/
# Check metrics from each cluster (with authentication)
TOKEN=$(kubectl create token vault-sync-operator-controller-manager -n vault-sync-operator-system)
kubectl port-forward -n vault-sync-operator-system svc/vault-sync-operator-controller-manager-metrics-service 8080:8080
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/metrics | grep vault_sync_operator