Skip to main content
RunBook Academy

CephCIV · Multi-Tenancy in PracticeMulti-Tenancy in Practice

Continuous isolation verification and cross-tenant incidents

Advanced⏱ ~18 mincephrbd

What you'll learn

  • Run isolation checks continuously rather than once
  • Detect a boundary that has weakened
  • Handle one tenant affecting others
  • Attribute impact to a tenant

Prerequisites

None — start here.

Verified against Ceph Tentacle 20.2.x · Ceph Squid 19.2.x (supported previous) · cephadm matches the verified Ceph release · podman 4.x · csi-rbd and csi-cephfs current · RBD / CephFS / RGW current (matches Ceph release) · Linux kernel 5.15+ (5.10 minimum) · Ubuntu 24.04 LTS (Ceph host baseline) · Debian 12 (Bookworm) (Ceph host baseline) · Rocky Linux / RHEL / AlmaLinux 9.x (Ceph host baseline) · Proxmox VE 9.x (cross-course integration) · Kubernetes 1.31+ (cross-course integration) · 2026-08-18

Not yet marked complete on this device.

Why this matters in production

Isolation verified once at launch drifts as capabilities are widened, pools are added, and tenants are migrated.

Continuous verification

#!/bin/bash
# isolation-check.sh — run on a schedule, output diffable
set -u
ceph auth ls --format json | python3 -c '
import sys,json,re
for e in json.load(sys.stdin)["auth_dump"]:
    ent = e["entity"]
    if not ent.startswith("client.") or ent == "client.admin": continue
    caps = e.get("caps", {})
    osd = caps.get("osd", "")
    scoped = ("pool=" in osd) or ("namespace=" in osd)
    star = "allow *" in " ".join(caps.values())
    flag = "UNSCOPED" if not scoped else ("STAR" if star else "ok")
    print("%-10s %-30s %s" % (flag, ent, osd[:70]))'
bash isolation-check.sh | grep -v '^ok'
Any entity that is neither pool-scoped nor namespace-scoped is a
boundary that does not exist, whatever the design document says.
# and the negative test, per tenant
for T in acme beta gamma; do
  for OTHER in acme beta gamma; do
    [ "$T" = "$OTHER" ] && continue
    if rbd -n "client.${T}-prod" -p "${OTHER}-prod-rbd" ls >/dev/null 2>&1; then
      echo "ISOLATION FAILURE: $T can read ${OTHER}-prod-rbd"
    fi
  done
done

Detecting a weakened boundary

SignalMeaning
An entity’s caps changed since the registerdrift, investigate
A new pool with no owner in the registerprovisioned outside the process
An entity with allow *the boundary is gone
A tenant reading another’s pool in the negative testactive failure
A shared entity across tenantsrevocation is now cross-tenant
diff <(bash isolation-check.sh) /secure/isolation-baseline.txt

One tenant affecting others

Isolation of access does not give isolation of performance. Tenants
share OSDs, network, and the OSD op queue.
ceph osd pool stats
pool acme-prod-rbd id 12
  client io 1.4 GiB/s rd, 890 MiB/s wr, 41k op/s rd, 12k op/s wr
pool beta-prod-rbd id 13
  client io 2.1 MiB/s rd, 900 KiB/s wr, 60 op/s rd, 22 op/s wr
ceph osd perf | sort -k2 -rn | head
ceph daemon osd.0 dump_historic_slow_ops 2>/dev/null | head -30
ControlEffect
RBD QoS per imagecaps a tenant’s IOPS and bandwidth at the client
mClock profilebalances client, recovery, and scrub, not tenant against tenant
Separate pools on separate device classesgenuine performance separation
Separate CRUSH rootsfull separation at the cost of capacity efficiency
rbd config image set acme-prod-rbd/vol01 rbd_qos_iops_limit 5000
rbd config image set acme-prod-rbd/vol01 rbd_qos_bps_limit $((200*1024*1024))
# or for every image in the pool
rbd config pool set acme-prod-rbd rbd_qos_iops_limit 5000
rbd config pool ls acme-prod-rbd
RBD QoS is enforced by librbd on the client, so it bounds a cooperating
tenant. A tenant running their own client can remove it.

Attributing impact

ceph osd pool stats --format json | python3 -c '
import sys,json
rows = []
for p in json.load(sys.stdin):
    io = p.get("client_io_rate", {})
    rows.append((p["pool_name"],
                 io.get("read_op_per_sec",0) + io.get("write_op_per_sec",0),
                 io.get("read_bytes_sec",0) + io.get("write_bytes_sec",0)))
for n, ops, bw in sorted(rows, key=lambda r: -r[1])[:8]:
    print("%-24s %8d op/s  %8.1f MiB/s" % (n, ops, bw/1048576))'
Per-pool I/O rates are what convert "the cluster is slow" into "this
tenant is doing 40k op/s", which is the difference between an
investigation and a conversation.

Quiz

Knowledge check · 4 questions

  1. Q1. Where is RBD QoS enforced?

  2. Q2. One tenant saturating the cluster affects every other tenant however well the capability boundaries are drawn.

  3. Q3. Investigate a cross-tenant performance complaint.

    Several tenants report slow storage. Capability scoping is correct and no OSD has failed.

  4. Q4. What does a continuous isolation check look for that a launch-time test does not?

Passing score: 75%. Answers are checked in this browser.

Production discipline

Run the isolation check on a schedule and diff it against a baseline — capabilities widen, pools appear, and a boundary verified at launch is not a boundary that still exists. Remember that access isolation is not performance isolation; only device classes or CRUSH roots give that.

Cross-course references

  • Kubernetes: NetworkPolicy isolation drifts the same way without continuous verification
  • Linux: a control verified once is a control assumed, not a control confirmed