Implementing multitenancy with Apicurio Registry

Implement multitenancy with Apicurio Registry on Kubernetes using the Apicurio Registry Operator. Apicurio Registry uses a multi-instance operator pattern. Each tenant receives a dedicated ApicurioRegistry3 custom resource (CR), which the operator reconciles into an independent set of Kubernetes resources. This approach provides strong isolation between tenants at the infrastructure level. It uses Kubernetes-native mechanisms for security, resource management, and network segmentation.

Prerequisites

Multitenancy overview

The Apicurio Registry Operator watches for ApicurioRegistry3 custom resources and creates the following Kubernetes resources for each CR:

Component Resources created

Apicurio Registry backend (REST API) component

Deployment, Service, Ingress, NetworkPolicy, PodDisruptionBudget

Apicurio Registry web console component

Deployment, Service, Ingress, NetworkPolicy, PodDisruptionBudget

Each CR instance is completely independent. Resources are named using the pattern {cr-name}-{component}-{resource-type} and labeled with instance-specific selectors to prevent any cross-tenant interference.

The multi-instance approach provides the following isolation boundaries:

  • Compute isolation: Separate Deployments and Pods per tenant

  • Network isolation: Separate Services, Ingresses, and optional NetworkPolicies per tenant

  • Storage isolation: Each tenant can use a separate database or Kafka cluster

  • Authentication isolation: Each tenant can have its own OIDC provider or realm

  • Configuration isolation: Environment variables and settings are per-CR

Deployment patterns

You can deploy multiple Apicurio Registry instances using three primary patterns.

Pattern A: Single namespace, multiple tenants

All tenant registry instances reside in the same namespace. The operator creates uniquely named resources for each CR.

Namespace: apicurio-registries
├── Operator Pod
├── ApicurioRegistry3: tenant-alpha
│   ├── tenant-alpha-app-deployment
│   ├── tenant-alpha-ui-deployment
│   └── ...
└── ApicurioRegistry3: tenant-beta
    ├── tenant-beta-app-deployment
    ├── tenant-beta-ui-deployment
    └── ...

This pattern is best for a small number of tenants managed by a single platform team.

Pattern B: Namespace per tenant (recommended)

Each tenant gets its own namespace. A single operator instance watches all namespaces.

Cluster
├── apicurio-system namespace
│   └── Operator Pod (watches all namespaces)
├── tenant-alpha namespace
│   └── ApicurioRegistry3: registry
└── tenant-beta namespace
    └── ApicurioRegistry3: registry

This pattern provides stronger isolation using Kubernetes RBAC, ResourceQuotas, and NetworkPolicies at the namespace level. It is the recommended approach for most production environments.

Pattern C: Operator per namespace

Each namespace has its own operator instance. The instance is restricted to watching only that namespace by using the APICURIO_OPERATOR_WATCHED_NAMESPACES environment variable.

Cluster
├── tenant-alpha namespace
│   ├── Operator Pod (APICURIO_OPERATOR_WATCHED_NAMESPACES="tenant-alpha")
│   └── ApicurioRegistry3: registry
└── tenant-beta namespace
    ├── Operator Pod (APICURIO_OPERATOR_WATCHED_NAMESPACES="tenant-beta")
    └── ApicurioRegistry3: registry

This pattern provides maximum isolation and is suitable when tenants manage their own operator lifecycle.

Deploying with namespace-per-tenant isolation

Deploy Apicurio Registry using the namespace-per-tenant pattern (Pattern B) to provide strong isolation between tenants.

Prerequisites
  • Cluster administrator access to a Kubernetes cluster is configured.

  • The Apicurio Registry Operator is installed. See Installing Apicurio Registry on OpenShift.

  • Storage infrastructure (PostgreSQL or Kafka) is available for each tenant.

Procedure
  1. Install the operator in a dedicated namespace. By default, it watches all namespaces:

    kubectl create namespace apicurio-system
    kubectl apply -f operator/install/install.yaml -n apicurio-system
  2. Verify that the operator is running:

    kubectl get pods -n apicurio-system
  3. Create tenant namespaces:

    kubectl create namespace tenant-alpha
    kubectl create namespace tenant-beta
  4. Create a database credentials Secret for each tenant:

    apiVersion: v1
    kind: Secret
    metadata:
      name: db-credentials
      namespace: tenant-alpha
    type: Opaque
    stringData:
      password: tenant-alpha-db-password
  5. Deploy an ApicurioRegistry3 CR for each tenant. For example, Tenant Alpha with PostgreSQL storage:

    apiVersion: registry.apicur.io/v1
    kind: ApicurioRegistry3
    metadata:
      name: registry
      namespace: tenant-alpha
    spec:
      app:
        storage:
          type: postgresql
          sql:
            dataSource:
              url: jdbc:postgresql://postgres-alpha.tenant-alpha.svc:5432/apicurio
              username: apicurio
              password:
                name: db-credentials
                key: password
        ingress:
          host: tenant-alpha-registry.apps.cluster.example
      ui:
        ingress:
          host: tenant-alpha-ui.apps.cluster.example

    And Tenant Beta with KafkaSQL storage:

    apiVersion: registry.apicur.io/v1
    kind: ApicurioRegistry3
    metadata:
      name: registry
      namespace: tenant-beta
    spec:
      app:
        storage:
          type: kafkasql
          kafkasql:
            bootstrapServers: "kafka-beta.tenant-beta.svc:9092"
        ingress:
          host: tenant-beta-registry.apps.cluster.example
      ui:
        ingress:
          host: tenant-beta-ui.apps.cluster.example
  6. Apply the custom resources:

    kubectl apply -f tenant-alpha-registry.yaml
    kubectl apply -f tenant-beta-registry.yaml
Verification
  • Check the status of all registry instances across the cluster:

    kubectl get apicurioregistries3 --all-namespaces
  • Verify that pods are running in each tenant namespace:

    kubectl get pods -n tenant-alpha
    kubectl get pods -n tenant-beta
  • Access each tenant’s Apicurio Registry web console using the configured hostnames and verify that the UI loads successfully.

Storage isolation options

Each tenant’s CR must point to a separate storage backend. The operator does not provision databases or Kafka topics automatically. Pre-provision storage before you deploy tenant CRs.

Separate databases on a shared PostgreSQL instance

Use one database per tenant on a shared PostgreSQL instance. The registry manages its own schema within each database. Use separate PostgreSQL users with permissions limited to each tenant’s database for additional security.

# Tenant Alpha
spec:
  app:
    storage:
      type: postgresql
      sql:
        dataSource:
          url: jdbc:postgresql://shared-postgres.infra.svc:5432/tenant_alpha
          username: tenant_alpha
          password:
            name: tenant-alpha-db-credentials
            key: password

# Tenant Beta
spec:
  app:
    storage:
      type: postgresql
      sql:
        dataSource:
          url: jdbc:postgresql://shared-postgres.infra.svc:5432/tenant_beta
          username: tenant_beta
          password:
            name: tenant-beta-db-credentials
            key: password
Separate PostgreSQL instances

For maximum storage isolation, each tenant can have its own PostgreSQL instance.

spec:
  app:
    storage:
      type: postgresql
      sql:
        dataSource:
          url: jdbc:postgresql://postgres-alpha.tenant-alpha.svc:5432/apicurio
          username: apicurio
          password:
            name: db-credentials
            key: password
KafkaSQL storage

When you use KafkaSQL storage, configure each tenant with separate Kafka bootstrap servers or, at minimum, separate topics.

spec:
  app:
    storage:
      type: kafkasql
      kafkasql:
        bootstrapServers: "kafka-alpha.tenant-alpha.svc:9092"
When using KafkaSQL, ensure that each tenant uses separate Kafka topics. The default topic name kafkasql-journal is the same for all instances. Configure separate topic names by using the APICURIO_KAFKASQL_TOPIC environment variable if tenants share a Kafka cluster.

Authentication options per tenant

Each tenant instance can be configured with its own authentication settings. Different tenants can use different identity providers or OIDC realms.

Shared Keycloak with separate realms
apiVersion: registry.apicur.io/v1
kind: ApicurioRegistry3
metadata:
  name: registry
  namespace: tenant-alpha
spec:
  app:
    auth:
      enabled: true
      appClientId: registry-api
      uiClientId: apicurio-registry
      authServerUrl: https://keycloak.example.com/realms/alpha
      redirectUri: https://tenant-alpha-ui.apps.cluster.example
      logoutUrl: https://tenant-alpha-ui.apps.cluster.example
      authz:
        enabled: true
        ownerOnlyEnabled: true
        groupAccessEnabled: true
        readAccessEnabled: true
        roles:
          source: token
          admin: sr-admin
          developer: sr-developer
          readOnly: sr-readonly
    ingress:
      host: tenant-alpha-registry.apps.cluster.example
  ui:
    ingress:
      host: tenant-alpha-ui.apps.cluster.example
Separate identity providers

Each tenant CR can reference a different OIDC provider by specifying different authServerUrl values.

# Tenant Alpha uses Keycloak
spec:
  app:
    auth:
      enabled: true
      authServerUrl: https://keycloak.example.com/realms/alpha

# Tenant Beta uses Microsoft Entra ID
spec:
  app:
    auth:
      enabled: true
      authServerUrl: https://login.microsoftonline.com/{tenant-id}/v2.0

Resource management per tenant

Each Apicurio Registry instance receives the following default resource requests and limits.

Table 1. Default resource allocation
Component CPU request CPU limit Memory request Memory limit

App

500m

1

512Mi

1Gi

UI

100m

200m

256Mi

512Mi

The total minimum per tenant is approximately 600m CPU and 768Mi memory.

Use podTemplateSpec to adjust resources for high-traffic or resource-constrained tenants:

spec:
  app:
    podTemplateSpec:
      spec:
        containers:
          - name: apicurio-registry-app
            resources:
              requests:
                cpu: "1"
                memory: 2Gi
              limits:
                cpu: "2"
                memory: 4Gi

You can independently scale each tenant instance:

spec:
  app:
    replicas: 3
  ui:
    replicas: 2

Use Kubernetes ResourceQuota objects to enforce per-namespace resource limits when using the namespace-per-tenant pattern:

apiVersion: v1
kind: ResourceQuota
metadata:
  name: tenant-quota
  namespace: tenant-alpha
spec:
  hard:
    requests.cpu: "2"
    requests.memory: 4Gi
    limits.cpu: "4"
    limits.memory: 8Gi
    pods: "10"

Network isolation options

The operator creates NetworkPolicy resources for each instance by default.

For additional security when using the namespace-per-tenant pattern, apply a default-deny policy in each tenant namespace:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: tenant-alpha
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress

Then selectively allow traffic for the registry pods:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-registry-ingress
  namespace: tenant-alpha
spec:
  podSelector:
    matchLabels:
      app.kubernetes.io/name: apicurio-registry
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: ingress-nginx
      ports:
        - protocol: TCP
          port: 8080

To limit the operator to watching only certain namespaces, set the APICURIO_OPERATOR_WATCHED_NAMESPACES environment variable on the operator Deployment:

env:
  - name: APICURIO_OPERATOR_WATCHED_NAMESPACES
    value: "tenant-alpha,tenant-beta"

When this variable is empty or unset, the operator watches all namespaces. When installed by using OLM, this is automatically derived from the olm.targetNamespaces annotation.

Lightweight isolation with groups

For scenarios where full instance-per-tenant isolation is not required, Apicurio Registry supports logical isolation within a single instance using groups and owner-based access control.

Each logical tenant uses one or more groups as their namespace within the registry. Users can only modify artifacts and groups they own.

Enable owner-based access control in the ApicurioRegistry3 CR:

spec:
  app:
    auth:
      enabled: true
      appClientId: registry-api
      uiClientId: apicurio-registry
      authServerUrl: https://keycloak.example.com/realms/registry
      redirectUri: https://registry-ui.apps.cluster.example
      logoutUrl: https://registry-ui.apps.cluster.example
      authz:
        enabled: true
        ownerOnlyEnabled: true
        groupAccessEnabled: true

With this configuration:

  • ownerOnlyEnabled restricts artifact modifications to the artifact’s creator

  • groupAccessEnabled restricts group modifications to the group’s creator

The following table compares the advantages and limitations of this approach:

Advantage Limitation

Shared compute and storage reduces overhead

Weaker isolation than separate instances

Faster tenant provisioning (create a group, not a deployment)

No separate storage per tenant

Single deployment to monitor and upgrade

A bug or outage affects all tenants

Lower per-tenant cost

No independent scaling per tenant

This configuration is suitable for development environments, internal teams, or scenarios where tenants do not require strict data or infrastructure isolation.