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/24inner; VNI 254254 ). The 2026-05-14project_oobi_no_vxlan_directiveLOCKED 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:
- Its primary OOBI overlay interface (100.127.252.x), and
- 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:
- Multus
NetworkAttachmentDefinitionresources (one per receiver entry), macvlan over a host VLAN sub-interface - NetworkPolicy ingress rules scoped to the receiver's protocols
- listen port + CIDR
- Strategic-merge patches to the target MÓDULO's Deployment
k8s.v1.cni.cncf.io/networksannotation 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_portonly, 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-fabricand 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
KnownTargetModulesaddition + 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.
Receiverresource 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 + testsk8s/oobi/82-customer-mgmt-vlan-daemonset.yaml— host-side VLAN sub-interface reconcilerk8s/oobi/83-customer-mgmt-receiver-controller.yaml— controller Deployment + RBACplatform/observability/customer-mgmt-receivers-cm.yaml— initial empty ConfigMapk8s/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).