The Incident That Started It All

At 09:42 on a Tuesday, ShitOps' quarterly tenant onboarding wave hit our event streaming platform. Tenant A's analytics workload began receiving a trickle of Tenant B's order lifecycle events. No data was lost, but our shared distributed proxy route table had reloaded while a tenant-specific listener was being registered. The proxy had been doing what we asked: forwarding event streams based on topic prefixes. The problem was that our prefixes were not authoritative across all tenants, and the reload path had no transactional boundary. We needed a tenant-aware event streaming fabric that could route every event through a distributed proxy with Podman-isolated data planes, without relying on a shared mutable route table.

flowchart TD A[Tenant Request] --> B[Edge Distributed Proxy Envoy + Wasm] B --> C{Tenant ID Present} C -- No --> D[Reject with 403] C -- Yes --> E[SPIFFE Identity Check] E --> F[OPA Policy Evaluation] F --> G[Podman Pod per Tenant] G --> H[Sidecar Event Stream Adapter] H --> I[Kafka Topic Namespace] H --> J[NATS JetStream Mirror] I --> K[Postgres Tenant Registry] J --> K K --> L[OpenTelemetry Collector] L --> M[Grafana Tempo]

Design Goals

We set four non-negotiable goals. First, every tenant gets a dedicated Podman pod boundary for its event streaming adapters. Second, every request traverses a distributed proxy that resolves tenant identity before any broker connection is opened. Third, all routing state is derived from a multi-tenant control plane with optimistic concurrency. Fourth, we can replay a tenant's event stream in a sandbox without touching production topics.

Architecture Overview

The resulting system, TenantMesh, is a multi-tenant event streaming fabric built around Podman pods and a distributed proxy tier. Each tenant receives a rootless Podman pod containing an event adapter, a policy sidecar, a metrics sidecar, and a local NATS JetStream leaf node. The edge distributed proxy is an Envoy fleet with WebAssembly filters for tenant extraction, SPIFFE/SPIRE identity verification, and OPA decision caching. Kafka remains the durable event streaming backbone; NATS JetStream acts as a tenant-local mirror for low-latency fan-out. A Postgres-backed tenant registry stores the canonical mapping of tenant to topic namespace, and a CRDT reconciler keeps proxy caches convergent across regions.

Podman-Powered Tenant Pods

Each tenant pod is declared as a Podman Quadlet unit and managed by systemd. We use rootless Podman with user namespaces, so each tenant's adapter runs under a distinct UID mapping. The adapter connects to Kafka with mTLS and to NATS with JWT-based credentials. A policy sidecar evaluates OPA bundles on every batch flush; a metrics sidecar exposes OpenTelemetry traces to the collector. Pods are placed on dedicated node pools labeled by tenant tier. Network isolation is enforced with netavark and per-pod firewall rules. We generate the Quadlet units from a Crossplane composition, which means tenant onboarding is a Git commit rather than a ticket.

Distributed Proxy Layer

The distributed proxy is the only ingress for tenant event streams. Every client connects to the nearest Envoy edge using HTTP/2 or gRPC. A Wasm filter extracts the tenant claim from the SPIFFE SVID, validates it against the tenant registry, and injects a routing header. OPA then evaluates a signed policy bundle: allowed topics, rate limits, schema version, and retention class. The proxy opens a connection to the tenant's Kafka namespace through a tenant-scoped upstream cluster. No shared route table exists. Route state is compiled into an immutable xDS snapshot and distributed with a versioned envelope. If a tenant changes, we publish a new snapshot; proxies keep serving the old snapshot until the new one is acknowledged by a quorum of edge nodes.

Event Streaming Backbone

Kafka provides durable, ordered event streaming. We create one topic namespace per tenant, with naming enforced by the proxy and by an ACL sync job. NATS JetStream mirrors selected subjects into the tenant pod for local fan-out and replay. A Temporal workflow orchestrates schema registration, ACL sync, mirror creation, and smoke tests. The workflow is idempotent and retries with exponential backoff. For cross-region tenants, a CRDT reconciler merges route deltas and resolves conflicts by policy version and timestamp. We also run a shadow consumer that verifies every produced event against the tenant schema before it is acknowledged upstream.

Multi-Tenant Control Plane

The control plane is a GraphQL federation gateway over tenant, policy, and stream services. It writes to Postgres with row-level security and emits change events to Kafka. A Dapr pub/sub component fans those changes out to reconcilers. Kyverno-style admission policies validate tenant manifests before they reach the reconciler. Crossplane compositions provision Podman pods, Envoy clusters, Kafka namespaces, and NATS mirrors. Every change is signed and audited. The multi-tenant registry stores tenant ID, SPIFFE trust domain, topic prefix, retention tier, and allowed egress. The registry is the source of truth; proxy caches are disposable.

Lifecycle and Reconciliation

Tenant onboarding starts with a pull request containing a TenantMesh manifest. CI runs schema compatibility checks, policy unit tests, and a dry-run reconciliation. Argo CD syncs the manifest to the control plane. Temporal executes the onboarding saga: create Kafka namespace, create NATS mirror, generate Podman Quadlet, deploy pod, warm proxy snapshot, run smoke test, enable traffic. Rollback is another saga. We keep three generations of proxy snapshots and can pin a tenant to a previous snapshot during an incident. The reconciler emits a heartbeat every 10 seconds; if a tenant pod misses three heartbeats, the proxy degrades that tenant to read-only and pages the on-call engineer.

Observability and SLOs

Every hop is instrumented with OpenTelemetry. The edge proxy emits span attributes for tenant ID, policy version, snapshot version, and decision latency. The event adapter emits batch size, flush duration, and broker error class. Grafana Tempo stores traces; Prometheus stores metrics; Loki stores structured logs. We define SLOs per tenant tier: p99 proxy decision latency under 40 ms, p99 end-to-end event delivery under 250 ms, and zero cross-tenant deliveries. A continuous canary tenant produces synthetic events every second and verifies isolation by attempting a forbidden topic read. The canary fails closed and triggers a snapshot rollback if isolation is breached.

Rollout Results

We rolled TenantMesh out to 42 tenants over six weeks. During the first canary week, the cross-tenant canary detected two policy cache misses; both were resolved by snapshot rollback before any production event was exposed. Proxy decision latency stayed under 31 ms at p99. Tenant onboarding time dropped from three days to eleven minutes. The Podman pod model gave us clean resource accounting per tenant, and the distributed proxy eliminated shared route table reloads. We now treat every tenant as a first-class event streaming boundary.

Lessons Learned

The biggest lesson was that identity must travel with the event. We now require SPIFFE SVIDs on every proxy hop and never trust a topic prefix alone. The second lesson was that route state should be immutable and versioned; xDS snapshots gave us deterministic rollbacks. The third lesson was that Podman Quadlets are an excellent fit for tenant-scoped systemd services. We are already planning TenantMesh v2 with per-tenant Wasm policy bundles, region-aware snapshot quorums, and a self-service replay console.