Skip to content

ADR 0046 — OOBI Hello payload encryption + host_id + engaged_in_test

  • Status: Accepted (2026-05-14)
  • Date: 2026-05-14
  • Deciders: TLSStress.Art project (operator directive 2026-05-14)
  • Builds on: ADR 0043 (OOBI VLAN), ADR 0044 (intrusion), ADR 0045 (Hello mesh)
  • Targets: v4.0.0

Context

ADR 0045 shipped the Hello/KeepAlive mesh on VLAN 2777 with a JSON-over-UDP wire format chosen for operator debuggability (tcpdump -i oobi0 udp port 47802 -A). On 2026-05-14 the operator captured a follow-up requirement:

Devemos implementar uma funcionalidade, onde o payload com mensagem contida no pacote KeepAlive, seja "embaralhada" criptografada, por uma chave de criptografia/decriptografia que seja de conhecimento somente dos elementos do software, para evitar que um SPAN port feito pelo operador, capture os pacotes na rede OOBI e consiga decifrar a mensagem contida no payload dos pacotes keepalive. Também juntamente no payload dos keepalives, ao gerar o pacote, também deve ser inserida identificacao do Host em que o elemento reside, assim como informar se naquele momento estava ou não engajado em algum teste de performance ativo.

Two distinct requirements:

  1. Confidentiality against a SPAN-port observer. The operator who configures a SPAN session on the customer's switch must NOT be able to read Hello payloads — they MUST see opaque bytes.
  2. Two new payload fields: host_id (which physical host the MÓDULO runs on) and engaged_in_test (whether the MÓDULO is currently servicing an operator performance test).

Decision

Cryptographic primitive

AES-256-GCM (RFC 5116). Rationale:

  • Stdlib (crypto/aes + crypto/cipher.NewGCM) — no third-party dep.
  • AEAD: confidentiality + authenticity in one pass. Tag mismatch = drop + alert.
  • 256-bit key + 96-bit nonce per packet. Fresh random nonce per Seal call.
  • Standard, audited, FIPS-validated.

Wire envelope

[12 bytes nonce] || [N bytes ciphertext] || [16 bytes GCM tag]

crypto/cipher.AEAD.Seal appends to the nonce slice → output starts with the nonce. Receiver slices off the first 12 bytes to use as nonce, passes the remainder to Open.

Key provisioning

Pre-shared key (PSK), 32 bytes / 256 bits, hex-encoded in the env var OOBI_HELLO_PSK. Deployed via a Kubernetes Secret mounted as env (NOT volume — env is the smallest blast radius for the value).

Bootstrap CLI: pkg/oobi/hello exposes GeneratePSK() (hex, error) so an operator can seed a fresh key once and propagate it to every MÓDULO Secret. No automatic rotation in MVP — manual rotation requires a coordinated rolling restart across all 6 MÓDULOs.

Fail-closed default

NewDefaultMonitor returns an error when OOBI_HELLO_PSK is unset and LabMode == false. Production MÓDULOs cannot start without the PSK — the confidentiality guarantee is enforced by the binary, not by hope. The existing --auth-lab-mode flag propagates to hello.LabMode so dev/lab boots without secrets continue to work, but the operator must explicitly request it.

New payload fields

{
  // ... existing ADR 0045 fields ...
  "host_id":         "node-1.us-east-1.k8s.local",
  "engaged_in_test": false
}
  • host_id — provisioned via OOBI_HELLO_HOST_ID env (Kubernetes downward API: valueFrom.fieldRef.fieldPath: spec.nodeName). Falls back to os.Hostname() when unset.
  • engaged_in_test — sourced from EngagementProvider func() bool injected per MÓDULO. Default = constant false (infra-only MÓDULOs). TREX.Art will plug its scenario orchestrator in a follow-up; other MÓDULOs may add their own once they grow active-test semantics.

Both fields live INSIDE the encrypted payload — an attacker who breaks confidentiality reads the slot too, no extra leak.

Metrics surface

Two new info-style gauges drive the orthogonal Dashboard map:

oobi_hello_module_info{detected_by, host}                      1
oobi_hello_engaged_in_test{detected_by}                        0|1
oobi_hello_neighbor_host_info{detected_by, neighbor_slot, neighbor_module, host}  1
oobi_hello_neighbor_engaged_in_test{detected_by, neighbor_slot, neighbor_module}  0|1
oobi_hello_decrypt_errors_total{detected_by}                   counter

Cardinality budget unchanged: ≤ (modules × neighbor_slots) ≈ 1369 per MÓDULO.

oobi_hello_decrypt_errors_total is the strongest intrusion signal in the mesh — every PSK-less attacker who reaches VLAN 2777 will tick this counter. Alertmanager rule shipped in PR-OOBI-HELLO-CRYPT-2.

Consequences

Positive

  • A SPAN-port observer sees only opaque bytes. Host topology, engagement state, neighbor list — all confidential.
  • Decryption-failure counter is a high-signal intrusion alert.
  • Fail-closed boot prevents accidental plaintext deployment.
  • host_id + engaged_in_test unlock the orthogonal Dashboard map (PR-OOBI-HELLO-CRYPT-2) and the "do not disturb that node — it's mid-test" UX.

Negative

  • tcpdump -A no longer inline-readable. Operators debugging the protocol must use hello-cli decrypt (TBD CLI in a follow-up) or temporarily start a MÓDULO in --auth-lab-mode against an isolated VLAN.
  • PSK rotation requires coordinated restart. MVP: manual.
  • Adds 28 bytes per packet (12 nonce + 16 tag). Negligible for a 30 s cadence.
  • One env var more in the deployment pipeline.

Neutral

  • AES-GCM is constant-time; no measurable CPU at 30 s × 6 MÓDULOs.

Rollout

  1. PR-OOBI-HELLO-CRYPT-1 (this PR) — library: cipher, packet fields, default.go env wiring, metrics, tests, ADR.
  2. PR-OOBI-HELLO-CRYPT-2 — Dashboard orthogonal host-grouped map UI + Grafana panel + Alertmanager rule for oobi_hello_decrypt_errors_total.
  3. Futurehello-cli decrypt utility for operator debugging; automatic PSK rotation when CONTROL plane supports it.

References

  • Memo: project_oobi_hello_encrypted_payload_2026_05_14.md
  • ADR 0045: OOBI Hello / KeepAlive mesh
  • ADR 0044: OOBI intrusion detection
  • Package: pkg/oobi/hello/crypto.go
  • Bootstrap secret template: k8s/oobi/05-hello-psk-secret.yaml