Skip to content

ADR 0041 — Customer MGMT Receiver Pattern

  • Status: Accepted (2026-05-13)
  • Date: 2026-05-13
  • Deciders: TLSStress.Art project
  • Targets: v3.9.x → v4.x — cross-cutting infrastructure for any MGMT-plane MÓDULO that needs to be reachable from the customer's DUT (FLOW.Art today, SyslogReflector.Art today, future SNMP receivers tomorrow)
  • Builds on: ADR 0037 (pkg/oobi/client/), ADR 0039 (SyslogReflector.Art), ADR 0040 (HyperBridge NetFlow synthesis)

Context

MGMT-plane MÓDULOs (FLOW.Art at OOBI slot .234, SyslogReflector.Art sidecar in promtail-syslog, future SNMP receivers) listen on the OOBI fabric.

Historical context (2026-05-10): at the time this ADR was written the OOBI fabric was a VXLAN overlay (100.127.252.0/24 inner; VNI 254254 ). The 2026-05-14 project_oobi_no_vxlan_directive LOCKED memo superseded that approach: the OOBI fabric is now a plain 802.1Q VLAN 2777 + 100.127.252.0/22 (RFC 6598 CGNAT). The decision recorded by this ADR — that customer-mgmt receivers live on the OOBI fabric — is unaffected; only the underlying transport changed.

That subnet is, by design, unreachable from anything off the OOBI fabric — including the customer's DUT.

Yet the entire point of these receivers is to ingest NetFlow / IPFIX / syslog from the DUT that the customer manages. The DUT lives on the customer's MGMT LAN — a normal 802.1q VLAN with the customer's own IP plan — NOT on our overlay.

Today (pre-ADR 0041), FLOW.Art works only when packets reach the overlay via RELAY.Art (slot .240/.241, ADR 0037), which is the "bridge OOBI↔customer-MGMT" piece. RELAY can route OOBI control traffic to the customer's MGMT but does NOT proxy bulk telemetry (NetFlow records can be 1000s/s; syslog can be 100s/s — both exceed RELAY's design point).

Architectural gap: a clean, generic way to dual-home any MGMT-plane MÓDULO Pod so it carries both:

  1. Its primary OOBI overlay interface (100.127.252.x), and
  2. A secondary interface on the customer's MGMT VLAN (operator-configurable IP plan), reachable from the DUT.

The pattern needs to:

  • Work for FLOW.Art (UDP) AND SyslogReflector.Art (UDP + TCP)
  • Be operator-configurable per-DUT (not a hard-coded VLAN baked into the manifests — different customers, different VLAN plans)
  • Handle DHCP-assigned AND static IPv4 + IPv6 addressing
  • Be safe in a multi-tenant lab (no IP-pool collisions across customers/DUTs)
  • Reuse our existing CNI tooling (Multus + macvlan + CNI dhcp daemon already shipping in k8s/dut/)

Decision

Ship a cross-cutting Go controller (pkg/customer-mgmt-receiver/) that reconciles a single operator-edited ConfigMap (customer-mgmt-receivers in oobi-fabric) into:

  1. Multus NetworkAttachmentDefinition resources (one per receiver entry), macvlan over a host VLAN sub-interface
  2. NetworkPolicy ingress rules scoped to the receiver's protocols
  3. listen port + CIDR
  4. Strategic-merge patches to the target MÓDULO's Deployment k8s.v1.cni.cncf.io/networks annotation to attach the new NAD

A companion host-side DaemonSet (k8s/oobi/82-customer-mgmt-vlan-daemonset.yaml) owns the ip link add ethN.<tag> type vlan creation on every node — the controller cannot do this from Pod-net-ns.

Six design decisions (FINAL)

# Decision Choice Rationale
1 IPAM default DHCP The CNI dhcp daemon is already deployed (k8s/dut/30-cni-dhcp-daemon.yaml). Operator can override to static via the mode: static field — required for environments without a DHCP server.
2 VLAN sub-interface creation DaemonSet A controller cannot ip link add from inside a Pod netns; the DaemonSet runs privileged on the host netns. Same pattern as 30-vtep-daemonset.yaml and 48-bpdu-guard-daemonset.yaml.
3 Multi-tenant scoping 1 receiver = 1 DUT No shared pools. The operator enumerates every (DUT, MÓDULO) pair explicitly. Eliminates implicit aliasing across customers; makes audit logs and revocation trivial.
4 NIC selection Operator-chosen The operator names host_nic: eth1 (or whatever physical NIC carries their trunk). Multiple receivers MAY share a NIC via different VLAN tags.
5 IPv4 + IPv6 dual-stack Required Customer MGMT LANs are routinely dual-stacked in 2026; we accept both static_ipv4 + static_ipv6 in the same entry. DHCP handles both families through the CNI dhcp plugin.
6 TLS for NetFlow NO Industry standard for NetFlow / IPFIX is UDP cleartext on a dedicated MGMT VLAN — segmentation is the security boundary, not transport encryption. The Protocol enum reserves tls for future use but Receiver.Validate() rejects it in this MVP.

Schema

# customer-mgmt-receivers ConfigMap (oobi-fabric namespace)
receivers:
  - name: prod-dut-east           # [a-z0-9-]{1,32}; unique
    target_module: flow-art        # flow-art | syslog-reflector
    host_nic: eth1                 # physical NIC carrying the trunk
    vlan_tag: 50                   # 1-4094, unique per host_nic
    ipam:
      mode: dhcp                   # dhcp (default) | static
      # The following 4 fields are ONLY for mode: static
      static_ipv4: ""              # CIDR, e.g. "192.168.50.234/24"
      static_ipv6: ""              # CIDR, e.g. "fd00:abcd::234/64"
      gateway_ipv4: ""             # IPv4 address
      gateway_ipv6: ""             # IPv6 address
    listen_port: 2055              # UDP/TCP port the receiver opens
    protocols: [udp]               # FLOW=udp; SyslogReflector=udp+tcp

MÓDULO → Deployment mapping

The target_module field names the MÓDULO. The actual K8s Deployment.metadata.name is sometimes different:

target_module Deployment.metadata.name Notes
flow-art flow-art Standalone deployment in oobi-fabric
syslog-reflector promtail-syslog Runs as a sidecar in promtail-syslog

The closed list is pkg/customer-mgmt-receiver/internal/types.KnownTargetModules. Expanding it is a one-line code change + Deployment-side label.

Opt-in label

Every MÓDULO Deployment that wants this pattern carries customer-mgmt-receiver.tlsstress.art/target-module: <name> on its pod template metadata. The controller patches in-place; the base k8s.v1.cni.cncf.io/networks value remains FIRST in the merged annotation so the OOBI overlay stays as the primary attachment.

Implementation — stdlib-only, kubectl-shell-out

The controller is stdlib-only + sibling pkg/oobi import. It does NOT use k8s.io/client-go. Instead it shells out to a pinned kubectl binary in the runtime image — same pattern as k8s/oobi/30-vtep-daemonset.yaml, k8s/dut/48-bpdu-guard-daemonset.yaml, and every other reconciler in this repo.

This keeps the go.mod hermetic, the image small (~50MB), the audit surface bounded, and the controller easy to review. The trade-off — one fork per action — is unfussy at the operator-edited reconcile cadence (30s default).

Threat model + security posture

  • Inbound: ingress is constrained by the per-receiver NetworkPolicy — UDP/TCP on listen_port only, peer restricted to the receiver's CIDR (or RFC1918 + ULA when DHCP — the kernel netfilter is the fine-grained gate when the CNI dhcp lease bounds aren't pre-known).
  • Outbound: receivers do NOT initiate traffic on the customer MGMT VLAN — they only listen. Multus macvlan in bridge mode permits outbound replies but the MÓDULOs don't generate any.
  • Operator-only configuration: the ConfigMap lives in oobi-fabric and is RBAC-restricted to operators with cluster edit access. The customer-mgmt-receiver controller's ClusterRole is scoped to one namespace (oobi-fabric) and cannot escalate.
  • Tier classification: Tier A (per ADR 0028 — operational plumbing, no patent-claimed primitives). Tier A code is open-source-compatible and ships in every distribution.

Consequences

Positive:

  • One operator-edited ConfigMap is the entire knob — clean control surface
  • FLOW.Art + SyslogReflector immediately benefit; future SNMP receivers get the pattern for free (one-line KnownTargetModules addition + the new MÓDULO's Deployment opt-in label)
  • DHCP default eliminates the IP-plan negotiation step in 80% of customer environments
  • Per-receiver NAD + NetworkPolicy = clean revocation: removing a ConfigMap entry deletes its NAD + NetworkPolicy + restores the base annotation, with no leftover artifacts
  • 1-receiver-per-DUT model maps directly to operator documentation patterns ("DUT X → MÓDULO Y → receiver Z")
  • Hermetic go.mod — no client-go dependency tree

Negative / accepted trade-offs:

  • DHCP-mode NetworkPolicy peer must be permissive (RFC1918 + ULA) because the CNI dhcp lease range isn't known at NAD-build time. Static IPAM gets the tighter peer policy (operator-supplied CIDR).
  • Operators must name physical NICs explicitly (host_nic: eth1) — no auto-discovery in the MVP. Future enhancement: probe and warn.
  • TLS for NetFlow not in MVP. Customers requiring it must wait for v2 (and reconsider — dedicated MGMT VLAN segmentation is the industry-accepted security boundary).
  • One Deployment patch per receiver-targeting-the-same-Deployment batches them, but kubectl-patch round-trips mean ~100ms per receiver during a fresh apply. Acceptable at lab cadence.

Out of scope (NOT this ADR):

  • The pkg/oobi/client/ mesh (ADR 0037) — orthogonal; OOBI client lives inside the Pod, customer-mgmt-receiver lives outside
  • HyperBridge.Art inline-tap (ADR 0036) — separate fail-to-wire primitive; can be deployed independently
  • SyslogReflector.Art internals (ADR 0039) — only the Deployment opt-in is touched here

Alternatives considered

  • Hard-coded VLAN in each MÓDULO manifest — Rejected. Every customer has a different MGMT VLAN; we'd need to edit every MÓDULO manifest per deployment.
  • Single NodePort Service per MÓDULO — Rejected. NodePort exposes the receiver on every node's primary NIC, leaking into the customer's data-plane VLAN; loses VLAN tagging and per-DUT scoping.
  • client-go controller with watch streams — Rejected. Adds ~250MB of indirect dependencies, ~10× the audit surface, and the polling cadence (30s) doesn't benefit from streaming. Pattern matches the rest of the repo (kubectl shell-out).
  • CRD + dedicated operator (e.g. Receiver resource per entry) — Rejected for MVP. ConfigMap-driven is simpler to audit, edit, and version. A CRD evolution is feasible in v2 without breaking the ConfigMap path.

Implementation plan

Wave Scope
CMR-1 (this PR) Pattern + 2 MÓDULOs (FLOW + SyslogReflector) opted in
CMR-2 Dashboard UI for editing the ConfigMap
CMR-3 SNMP receiver MÓDULO (when shipped) auto-uses the pattern
CMR-4 Optional per-receiver Prometheus metrics + Grafana panel

References

  • pkg/customer-mgmt-receiver/ — controller source + tests
  • k8s/oobi/82-customer-mgmt-vlan-daemonset.yaml — host-side VLAN sub-interface reconciler
  • k8s/oobi/83-customer-mgmt-receiver-controller.yaml — controller Deployment + RBAC
  • platform/observability/customer-mgmt-receivers-cm.yaml — initial empty ConfigMap
  • k8s/dut/30-cni-dhcp-daemon.yaml — CNI dhcp daemon (Decision #1 dependency)
  • k8s/oobi/30-vtep-daemonset.yaml — DaemonSet shell-out pattern this work mirrors

Last verified against shipping code: v3.9.0 (2026-05-13).