monedula-gitops docs menu
Enforce tenancy with admission webhooks
Restrict which topic names a namespace may claim with a KafkaCluster tenancy policy, and reject duplicate topic identities at admission.
On this page
A tenancy policy on a KafkaCluster decides which namespaces may use the cluster and which topic name prefixes each namespace may claim. Tenancy is an operator control: the CLI checks only the shape of a tenancy block.
Before you start
- The operator with the admission webhook enabled (
--set webhook.enabled=true). The webhook needs a serving certificate, issued by cert-manager by default or brought by you, as Configure the Kubernetes operator shows. - Control over the
KafkaClusterresources. A team that can create its ownKafkaClusterfor the same brokers bypasses the policy, so keep everyKafkaClusterin a namespace that tenants cannot write to, with the Helm valueclusterNamespace.
Steps
1. Add a tenancy policy to the KafkaCluster
This KafkaCluster lets only the namespace monedula-e2e reference it, and restricts that namespace to topic names that start with payments.. Save it as manifests/cluster.yaml:
apiVersion: gitops.monedula.dev/v1alpha1
kind: KafkaCluster
metadata:
name: tenant
spec:
bootstrapServers: host.docker.internal:9096
auth:
mechanism: SCRAM-SHA-512
scram:
username:
valueFrom:
secretKeyRef:
name: kafka-credentials
key: kafka-username
password:
valueFrom:
secretKeyRef:
name: kafka-credentials
key: kafka-password
tenancy:
allowedNamespaces:
- monedula-e2e
topicPrefixes:
- namespaces:
- monedula-e2e
prefixes:
- payments.
The bootstrap address, the Secret kafka-credentials and the namespace monedula-e2e come from the runnable scenario’s test environment; use your own, and run the kubectl commands on this page in the tenant namespace.
The policy has two levels:
allowedNamespaceslists glob patterns, such asteam-payments-*, for the namespaces whose resources of any kind may reference this cluster. When it is empty, any namespace may.- Each
topicPrefixesrule maps namespace glob patterns to allowed name prefixes. A namespace that matches at least one rule is prefix-restricted: it may only manage names that start with one of the prefixes of all rules it matches. A namespace that matches no rule is limited only byallowedNamespaces.
For a prefix-restricted namespace, the prefixes apply to more than topic names:
- Consumer group names reuse the topic prefixes: the groups in a topic’s
accessblock,grouprules in aKafkaAccessPolicyandGroupresources of aKafkaRoleBindingmust match them too.KafkaAccessPolicyrules are checked whether their pattern is literal or prefixed. - Grants that a name prefix cannot scope are denied:
KafkaAccessPolicyrules on thecluster,transactionalIdordelegationTokenresource, cluster-scoped role bindings and role-binding resources other thanTopicandGroup. Allowing them would let a team escalate past its prefix, for example withAlteron the cluster resource. KafkaQuotaandKafkaUserget no prefix check, because quota entities and usernames have no prefix structure. OnlyallowedNamespaceslimits them, so any allowed namespace may declare a quota or a user for any entity; a second claim on an entity that another team already declares is a duplicate identity, handled as in step 4.
2. Apply the cluster first
kubectl apply -f manifests/cluster.yaml
The webhook reads the tenancy policy from the KafkaCluster when it checks a topic. A topic whose cluster does not exist yet passes admission, and the operator checks it when it reconciles.
3. Try a topic outside the prefix
This topic in the restricted namespace claims the name forbidden.topic. Save it as manifests/topic.yaml:
apiVersion: gitops.monedula.dev/v1alpha1
kind: KafkaTopic
metadata:
name: tenancy-bad
spec:
clusterRef:
name: tenant
topicName: forbidden.topic
partitions: 1
kubectl apply -f manifests/topic.yaml
The webhook rejects the request with a message that names the topic name, the namespace and the allowed prefixes, here payments..
The webhook checks tenancy for KafkaTopic only. A violating resource of another kind, or any violating topic when the webhook is off, is created and then rejected by the operator: it gets the phase Error and the condition ValidationFailed=True with the reason TenancyDenied, and the operator does not touch Kafka for it until its spec, its namespace or the tenancy policy changes. Tenancy never blocks a deletion. A denied KafkaTopic, KafkaAccessPolicy or KafkaRoleBinding still counts as an owner of the ACLs and role bindings it declares, so the prunes of other resources on the cluster leave them in place.
4. Reject a duplicate topic identity
A topic’s identity is its cluster and its topicName, and only one KafkaTopic may claim it. These two topics both claim dup.orders on the cluster shared. Save them as manifests/a.yaml and manifests/b.yaml:
apiVersion: gitops.monedula.dev/v1alpha1
kind: KafkaTopic
metadata:
name: dup-orders-a
spec:
clusterRef:
name: shared
topicName: dup.orders
partitions: 1
apiVersion: gitops.monedula.dev/v1alpha1
kind: KafkaTopic
metadata:
name: dup-orders-b
spec:
clusterRef:
name: shared
topicName: dup.orders
partitions: 1
Apply the first, then the second:
kubectl apply -f manifests/a.yaml
kubectl apply -f manifests/b.yaml
The first is created. The webhook rejects the second with a message that contains topic identity (cluster, topicName) must be unique and names the topic that already holds the identity. Duplicate KafkaQuota, KafkaRoleBinding and KafkaUser identities are rejected the same way.
Without the webhook, the operator detects the duplicate when it reconciles. The older resource, by creationTimestamp and then by namespace and name, wins and keeps working. The newer one gets ValidationFailed=True with the reason DuplicateIdentity, Ready=False and a Warning event, and never touches Kafka. Delete the newer resource to resolve it. If you delete the older one instead, the newer takes over at its next resync. Deleting either of two duplicate KafkaUser or KafkaQuota resources leaves the credential or quota in place for the other, with an OrphanedToCoClaimant event.
The CRDs keep identity fields immutable with or without the webhook: a topic’s topicName and clusterRef, a policy’s clusterRef, a quota’s entity, a role binding’s clusterRef, principal, role and scope.type, and a user’s username and clusterRef. Renaming a topic or moving it to another cluster is rejected.
Check it worked
kubectl get kafkatopickubectl get kafkatopic tenancy-bad -o jsonpath='{.status.conditions}'With the webhook on, dup-orders-a is listed, and neither tenancy-bad nor dup-orders-b exists, because a rejected request is never stored. With the webhook off, tenancy-bad exists with ValidationFailed set to True and the reason TenancyDenied, and Kafka has no topic forbidden.topic.
Related
- The KafkaCluster reference lists every
tenancyfield. - Runnable versions: scenarios/15-tenancy-denied and scenarios/03-webhook-reject-duplicate-identity