Skip to content

Single-Tenancy

Step-by-step guide for deploying EvalHub with spec.tenancy: single on OpenShift. EvalHub runs in the same namespace as the workloads it serves — ideal for a single team or project that does not need a shared control-plane instance.

For shared concepts — tenant isolation, SAR authorisation, ClusterRoles, and common troubleshooting — see the Tenancy overview.

Before setting up single-tenancy, ensure you have:

  • The TrustyAI Operator installed and reconciling
  • Namespace-admin access in the workload namespace (no cluster-admin required for tenant labelling)
  • A namespace for your team or project (e.g. team-a)
  1. Deploy EvalHub

    Create an EvalHub instance in the workload namespace:

    apiVersion: trustyai.opendatahub.io/v1alpha1
    kind: EvalHub
    metadata:
    name: evalhub
    namespace: team-a
    spec:
    tenancy: single
    replicas: 1
    database:
    type: sqlite
    providers:
    - lm-evaluation-harness
    - garak
    - garak-kfp
    - guidellm
    - lighteval
    - ibm-clear
    collections:
    - leaderboard-v2
    - safety-and-fairness-v1
    - toxicity-and-ethical-principles

    For production workloads, use PostgreSQL instead of SQLite:

    database:
    type: postgresql
    secret: evalhub-db-credentials

    The operator automatically creates in the instance namespace:

    ResourcePurpose
    evalhub-service SAAPI server identity
    evalhub-team-a-job SAIdentity for evaluation job pods
    ClusterRole RoleBindingsjobs-writer, job-config, auth-reviewer, MLFlow
    evalhub-tenant-admin RoleFull CRUD on virtual EvalHub resources; MLflow read-only (get, list)
    evalhub-user RoleRead providers/collections, submit via evalhubs/proxy, create status-events, MLflow create/get
    evalhub-tenant-admin-bindingBinds admin Role to system:serviceaccounts:team-a
  2. Grant user access

    Unlike multi-tenancy, you do not need to write a custom evalhub-evaluator Role. The operator creates evalhub-user and evalhub-tenant-admin for you. Bind your principals to evalhub-user for standard evaluation access.

    EvalHub supports three principal types:

    PrincipalToken sourceRoleBinding kindHas home namespace?
    ServiceAccountoc create tokenServiceAccountYes (where SA lives)
    OpenShift Useroc whoami -t (OAuth)UserNo
    OpenShift Groupoc whoami -t (OAuth, via member)GroupNo

    Create the ServiceAccount:

    Terminal window
    oc apply -f - <<EOF
    apiVersion: v1
    kind: ServiceAccount
    metadata:
    name: team-a-user
    namespace: team-a
    EOF

    Bind to the operator-created evalhub-user Role:

    Terminal window
    oc apply -f - <<EOF
    apiVersion: rbac.authorization.k8s.io/v1
    kind: RoleBinding
    metadata:
    name: evalhub-user-binding
    namespace: team-a
    roleRef:
    apiGroup: rbac.authorization.k8s.io
    kind: Role
    name: evalhub-user
    subjects:
    - kind: ServiceAccount
    name: team-a-user
    namespace: team-a
    EOF
  3. Access the API

    All evaluation API requests must include the X-Tenant header set to the EvalHub instance namespace. In single-tenancy mode, this is always the namespace where EvalHub is deployed.

    Get the EvalHub route:

    Terminal window
    EVALHUB_URL=$(oc get route evalhub -n team-a -o jsonpath='{.spec.host}')

    Get a token:

    Terminal window
    TOKEN=$(oc create token team-a-user -n team-a --duration=1h)

    List providers:

    Terminal window
    evalhub providers list --token $TOKEN --tenant team-a

    Submit an evaluation job:

    Terminal window
    evalhub eval run \
    --token $TOKEN \
    --tenant team-a \
    --name mmlu-eval \
    --model-url http://vllm-server.team-a.svc.cluster.local:8000/v1 \
    --model-name meta-llama/Llama-3.2-1B-Instruct \
    --provider lm_evaluation_harness \
    --benchmark mmlu

    The job pod is created in team-a using the evalhub-team-a-job ServiceAccount.

In single-tenancy mode, the operator creates these namespace-scoped Roles (garbage-collected when the EvalHub CR is deleted):

API groupResourcesVerbs
trustyai.opendatahub.ioevaluations, collections, providers, status-eventsget, list, watch, create, update, patch, delete
trustyai.opendatahub.ioevalhubs/proxy (scoped to instance name)get, create
mlflow.kubeflow.orgexperimentsget, list

MLflow access is read-only — no create verb. Although this Role includes evaluations create, POST /api/v1/evaluations/jobs also performs SAR checks for MLflow experiments with create and get. Bind principals to evalhub-user for MLflow submission permissions.

Bound to system:serviceaccounts:{namespace} via evalhub-tenant-admin-binding, so all ServiceAccounts in the namespace receive admin access by default.

API groupResourcesVerbs
trustyai.opendatahub.iocollections, providersget, list, watch
trustyai.opendatahub.ioevalhubs/proxy (scoped to instance name)get, create
trustyai.opendatahub.iostatus-eventscreate
mlflow.kubeflow.orgexperimentsget, list, create

Bind individual principals to this Role for standard evaluation access without full admin permissions.

You can add custom providers and collections in the instance namespace without modifying the operator namespace. Label ConfigMaps with:

  • Providers: trustyai.opendatahub.io/evalhub-provider-type=tenant
  • Collections: trustyai.opendatahub.io/evalhub-collection-type=tenant

The operator discovers these in-place and mounts them at /config/providers/tenant/ and /config/collections/tenant/ — separate from system configs at /config/providers/ and /config/collections/ to avoid shadowing.

Changes to tenant ConfigMaps trigger reconciliation via the operator’s ConfigMap watch; no CR edit is required.

In single-tenancy mode, the operator does not:

  • Watch for evalhub.trustyai.opendatahub.io/tenant labels on other namespaces
  • Provision job ServiceAccounts or RoleBindings in namespaces other than the instance namespace
  • Create evalhub-discovery ConfigMaps in other namespaces
Terminal window
# Convenience Roles created by the operator
oc get role evalhub-tenant-admin evalhub-user -n team-a
oc get rolebinding evalhub-tenant-admin-binding -n team-a
# Job SA in the instance namespace
oc get sa evalhub-team-a-job -n team-a
# Confirm no EvalHub resources in a second namespace
oc get sa,role,rolebinding -n team-b 2>/dev/null | grep evalhub || echo "No cross-namespace resources (expected)"

Test API access with the correct tenant header:

Terminal window
oc auth can-i get providers.trustyai.opendatahub.io \
-n team-a \
--as=system:serviceaccount:team-a:team-a-user

For common issues such as 403 Forbidden responses, see Troubleshooting on the Tenancy overview.

In single-tenancy mode, X-Tenant must be set to the EvalHub instance namespace — not a different namespace. A request with X-Tenant: other-ns is rejected unless the caller has a RoleBinding in other-ns.

Requests without the X-Tenant header are rejected even though there is only one tenant. Always set X-Tenant to the instance namespace.

If a ServiceAccount can access the API without an explicit evalhub-user RoleBinding, it is likely receiving permissions through evalhub-tenant-admin-binding (which binds all namespace ServiceAccounts to admin). To enforce least privilege, remove or avoid relying on the default binding and bind principals explicitly to evalhub-user.


For shared control-plane deployments serving multiple teams, see Multi-Tenancy.