| Crates.io | zopp-operator |
| lib.rs | zopp-operator |
| version | 0.1.1 |
| created_at | 2026-01-05 18:03:48.322864+00 |
| updated_at | 2026-01-05 19:22:23.015919+00 |
| description | Kubernetes operator for zopp secrets manager |
| homepage | https://github.com/faiscadev/zopp |
| repository | https://github.com/faiscadev/zopp |
| max_upload_size | |
| id | 2024318 |
| size | 122,180 |
Kubernetes operator that automatically syncs secrets from zopp to Kubernetes Secrets using an annotation-based approach.
The operator watches all Kubernetes Secrets and syncs those annotated with zopp.dev/sync: "true".
The operator implements two concurrent synchronization mechanisms:
Event Streaming (Primary) - Real-time updates via gRPC streaming
Periodic Polling (Safeguard) - Reconciles every 60 seconds
This combines real-time performance with reliability guarantees.
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
namespace: default
annotations:
zopp.dev/sync: "true"
zopp.dev/workspace: "acme"
zopp.dev/project: "backend"
zopp.dev/environment: "production"
type: Opaque
data: {} # Will be populated by operator
# Using default credentials (~/.zopp/credentials.json)
cargo run --bin zopp-operator
# Using custom credentials
cargo run --bin zopp-operator -- --credentials /path/to/credentials.json
# Watch specific namespace only
cargo run --bin zopp-operator -- --namespace default
# Custom server address
cargo run --bin zopp-operator -- --server http://zopp-server:50051
# Check Secret data is populated
kubectl get secret app-secrets -o yaml
# Watch operator logs
kubectl logs -f deployment/zopp-operator -n zopp-operator-system
--server <URL> Zopp server address (default: http://127.0.0.1:50051)
--credentials <PATH> Path to credentials file (default: ~/.zopp/credentials.json)
--namespace <NAME> Namespace to watch (default: all namespaces)
| Annotation | Description | Example |
|---|---|---|
zopp.dev/sync |
Enable sync (must be "true") |
"true" |
zopp.dev/workspace |
Zopp workspace name | "acme" |
zopp.dev/project |
Zopp project name | "backend" |
zopp.dev/environment |
Zopp environment name | "production" |
The operator authenticates as a service principal. Follow these steps to set it up:
As a workspace administrator, create a service principal for the operator:
# Alice creates a service principal for the operator
zopp principal create k8s-operator --service
This creates a service principal with no user association (user_id = NULL).
The operator uses the standard zopp configuration file (~/.zopp/config.json). You can either:
Option A: Use the same config as your user
# The operator will use the service principal from your config
kubectl create secret generic zopp-config \
--from-file=config.json=$HOME/.zopp/config.json \
-n zopp-operator-system
Option B: Create a dedicated config file
# Switch to the service principal
zopp principal use k8s-operator
# Export just this principal's config
cp ~/.zopp/config.json /tmp/operator-config.json
# Create K8s secret
kubectl create secret generic zopp-config \
--from-file=config.json=/tmp/operator-config.json \
-n zopp-operator-system
The service principal needs access to the workspace:
# Create an invite for the workspace
zopp invite create --workspace acme --plain > invite.txt
# Use the invite to add the service principal to the workspace
# (This wraps the workspace KEK for the service principal)
zopp join $(cat invite.txt) k8s-operator@acme.com --principal k8s-operator
The operator will fetch and unwrap the workspace KEK at runtime using its X25519 keys.
┌─────────────────────────────────────────────────────────────────┐
│ Kubernetes Cluster │
│ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ Secret (annotated) │ │ Secret (annotated) │ │
│ │ zopp.dev/sync: true │ │ zopp.dev/sync: true │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ ▲ ▲ │
│ │ │ │
│ │ ┌────────────────────────┤ │
│ │ │ │ │
│ └────────┴────────────────────────┘ │
│ │ │
│ ┌────────▼──────────┐ │
│ │ Zopp Operator │ │
│ │ ┌──────────────┐ │ │
│ │ │Event Stream │ │ Real-time updates │
│ │ │(gRPC) │◄┼─────────────────────┐ │
│ │ └──────────────┘ │ │ │
│ │ ┌──────────────┐ │ │ │
│ │ │60s Reconcile │ │ Periodic poll │ │
│ │ │Loop │◄┼─────────────────┐ │ │
│ │ └──────────────┘ │ │ │ │
│ └───────────────────┘ │ │ │
└─────────────────────────────────────────────────┼───┼───────────┘
│ │
│ │
┌───────▼───▼────────┐
│ Zopp Server │
│ ┌──────────────┐ │
│ │ EventBus │ │
│ └──────────────┘ │
│ ┌──────────────┐ │
│ │ Secrets DB │ │
│ └──────────────┘ │
└────────────────────┘
The operator needs:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: zopp-operator
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "watch", "patch"]
Note: The operator has read/write access to Secret values, so credentials must be protected.
In production, store credentials in a Kubernetes Secret:
kubectl create secret generic zopp-credentials \
--from-file=credentials.json \
-n zopp-operator-system
Mount in deployment:
spec:
containers:
- name: operator
volumeMounts:
- name: credentials
mountPath: /etc/zopp
readOnly: true
env:
- name: ZOPP_CREDENTIALS
value: /etc/zopp/credentials.json
volumes:
- name: credentials
secret:
secretName: zopp-credentials
See deployment manifests for production deployment.
# Run locally (requires running zopp server)
cargo run --bin zopp-operator
# Run tests
cargo test --package zopp-operator
# Check code
cargo clippy --package zopp-operator
Zopp uses annotations on existing Secrets rather than custom resources:
| Feature | CRD Approach | Zopp (Annotations) |
|---|---|---|
| Sync Method | Polling-based | Event streaming + 60s poll |
| Latency | Seconds to minutes | < 1 second (stream), max 60s (poll) |
| API Load | Multiple requests/min | 1 persistent connection |
| User Experience | Learn new CRD types | Annotate existing Secrets |
| Kubernetes Native | New resource type | Standard Secret resource |
| State Separation | ✅ spec/status | ❌ Mixed |
Both approaches have tradeoffs - we may add CRD support later for users who prefer explicit state separation.
zopp.dev/keys annotation)